@ollang-dev/sdk 0.3.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.
Files changed (64) hide show
  1. package/bin/tms.js +47 -0
  2. package/dist/browser/index.d.ts +143 -0
  3. package/dist/browser/index.js +2336 -0
  4. package/dist/browser/ollang-browser.min.js +1 -0
  5. package/dist/browser/script-loader.d.ts +1 -0
  6. package/dist/browser/script-loader.js +53 -0
  7. package/dist/client.d.ts +13 -0
  8. package/dist/client.js +60 -0
  9. package/dist/index.d.ts +34 -0
  10. package/dist/index.js +74 -0
  11. package/dist/resources/cms.d.ts +29 -0
  12. package/dist/resources/cms.js +34 -0
  13. package/dist/resources/customInstructions.d.ts +11 -0
  14. package/dist/resources/customInstructions.js +24 -0
  15. package/dist/resources/orders.d.ts +13 -0
  16. package/dist/resources/orders.js +65 -0
  17. package/dist/resources/projects.d.ts +8 -0
  18. package/dist/resources/projects.js +29 -0
  19. package/dist/resources/revisions.d.ts +9 -0
  20. package/dist/resources/revisions.js +18 -0
  21. package/dist/resources/scans.d.ts +38 -0
  22. package/dist/resources/scans.js +52 -0
  23. package/dist/resources/uploads.d.ts +8 -0
  24. package/dist/resources/uploads.js +25 -0
  25. package/dist/tms/config.d.ts +16 -0
  26. package/dist/tms/config.js +172 -0
  27. package/dist/tms/detector/audio-detector.d.ts +27 -0
  28. package/dist/tms/detector/audio-detector.js +168 -0
  29. package/dist/tms/detector/auto-detect.d.ts +1 -0
  30. package/dist/tms/detector/auto-detect.js +152 -0
  31. package/dist/tms/detector/cms-detector.d.ts +17 -0
  32. package/dist/tms/detector/cms-detector.js +94 -0
  33. package/dist/tms/detector/content-type-detector.d.ts +79 -0
  34. package/dist/tms/detector/content-type-detector.js +2 -0
  35. package/dist/tms/detector/hardcoded-detector.d.ts +11 -0
  36. package/dist/tms/detector/hardcoded-detector.js +154 -0
  37. package/dist/tms/detector/i18n-detector.d.ts +16 -0
  38. package/dist/tms/detector/i18n-detector.js +311 -0
  39. package/dist/tms/detector/image-detector.d.ts +11 -0
  40. package/dist/tms/detector/image-detector.js +170 -0
  41. package/dist/tms/detector/text-detector.d.ts +9 -0
  42. package/dist/tms/detector/text-detector.js +35 -0
  43. package/dist/tms/detector/video-detector.d.ts +12 -0
  44. package/dist/tms/detector/video-detector.js +188 -0
  45. package/dist/tms/index.d.ts +12 -0
  46. package/dist/tms/index.js +38 -0
  47. package/dist/tms/multi-content-tms.d.ts +42 -0
  48. package/dist/tms/multi-content-tms.js +230 -0
  49. package/dist/tms/server/index.d.ts +1 -0
  50. package/dist/tms/server/index.js +1473 -0
  51. package/dist/tms/server/strapi-pusher.d.ts +31 -0
  52. package/dist/tms/server/strapi-pusher.js +296 -0
  53. package/dist/tms/server/strapi-schema.d.ts +47 -0
  54. package/dist/tms/server/strapi-schema.js +93 -0
  55. package/dist/tms/tms.d.ts +48 -0
  56. package/dist/tms/tms.js +875 -0
  57. package/dist/tms/types.d.ts +189 -0
  58. package/dist/tms/types.js +2 -0
  59. package/dist/tms/ui-dist/assets/index-5U1Hw3uo.css +1 -0
  60. package/dist/tms/ui-dist/assets/index-HvrqZV5Z.js +149 -0
  61. package/dist/tms/ui-dist/index.html +14 -0
  62. package/dist/types/index.d.ts +174 -0
  63. package/dist/types/index.js +2 -0
  64. package/package.json +98 -0
@@ -0,0 +1,2336 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OllangBrowser = void 0;
4
+ class OllangBrowser {
5
+ constructor(config) {
6
+ this.observer = null;
7
+ this.capturedContent = new Map();
8
+ this.i18nTexts = new Set();
9
+ this.i18nNormalized = new Set();
10
+ this.excludedTexts = new Set();
11
+ this.selectedContentIds = new Set();
12
+ this.folders = [];
13
+ this.selectedFolder = '';
14
+ this.strapiContentMap = new Map();
15
+ this.strapiMediaMap = new Map();
16
+ this.detectedStrapiUrls = new Set();
17
+ this.strapiEntries = new Map();
18
+ this.strapiLongTextMap = new Map();
19
+ this.capturedTexts = new Map();
20
+ this.apiKeyStorageKey = 'ollang_browser_api_key';
21
+ this.config = {
22
+ baseUrl: 'http://localhost:5972',
23
+ autoDetectCMS: true,
24
+ captureSelectors: [
25
+ 'h1',
26
+ 'h2',
27
+ 'h3',
28
+ 'h4',
29
+ 'h5',
30
+ 'h6',
31
+ 'p',
32
+ 'span',
33
+ 'div',
34
+ 'a',
35
+ 'button',
36
+ 'li',
37
+ 'td',
38
+ 'th',
39
+ 'label',
40
+ ],
41
+ excludeSelectors: [
42
+ 'script',
43
+ 'style',
44
+ 'noscript',
45
+ '.no-translate',
46
+ '#ollang-debug-panel',
47
+ '.ollang-debug-panel',
48
+ '[id^="ollang-"]',
49
+ '[class*="ollang-"]',
50
+ ],
51
+ captureAttributes: ['data-cms-field', 'data-cms-id', 'data-field-id'],
52
+ debounceMs: 2000,
53
+ onContentDetected: () => { },
54
+ ...config,
55
+ };
56
+ if (!this.config.apiKey && typeof window !== 'undefined') {
57
+ const stored = this.getStoredApiKey();
58
+ if (stored) {
59
+ this.config.apiKey = stored;
60
+ }
61
+ }
62
+ this.selectedFolder = config.selectedFolder || '';
63
+ this.init();
64
+ }
65
+ getStoredApiKey() {
66
+ try {
67
+ if (typeof window === 'undefined')
68
+ return null;
69
+ const raw = window.localStorage.getItem(this.apiKeyStorageKey);
70
+ return raw && raw.trim() ? raw : null;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ saveApiKey(key) {
77
+ try {
78
+ if (typeof window === 'undefined')
79
+ return;
80
+ window.localStorage.setItem(this.apiKeyStorageKey, key);
81
+ }
82
+ catch { }
83
+ }
84
+ init() {
85
+ if (typeof window === 'undefined') {
86
+ throw new Error('OllangBrowser can only be used in browser environment');
87
+ }
88
+ this.interceptApiCalls();
89
+ this.loadI18nFiles().then(() => {
90
+ console.log(`✅ Loaded ${this.i18nTexts.size} i18n texts from files`);
91
+ this.detectFrameworkI18n();
92
+ setTimeout(() => {
93
+ console.log(`🔍 Starting capture with ${this.i18nTexts.size} i18n texts and ${this.strapiContentMap.size} Strapi contents tracked`);
94
+ this.startCapture();
95
+ }, 3000);
96
+ });
97
+ }
98
+ interceptApiCalls() {
99
+ this.interceptFetch();
100
+ this.interceptXHR();
101
+ }
102
+ interceptFetch() {
103
+ const self = this;
104
+ const originalFetch = window.fetch.bind(window);
105
+ window.fetch = function (input, init) {
106
+ const url = typeof input === 'string' ? input : input?.url || String(input);
107
+ return originalFetch(input, init).then((response) => {
108
+ if (self.isStrapiApiUrl(url)) {
109
+ response
110
+ .clone()
111
+ .json()
112
+ .then((data) => {
113
+ self.processStrapiResponse(url, data);
114
+ })
115
+ .catch(() => { });
116
+ }
117
+ return response;
118
+ });
119
+ };
120
+ }
121
+ interceptXHR() {
122
+ const self = this;
123
+ const originalOpen = XMLHttpRequest.prototype.open;
124
+ const originalSend = XMLHttpRequest.prototype.send;
125
+ XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
126
+ this._ollangUrl = typeof url === 'string' ? url : String(url);
127
+ return originalOpen.call(this, method, url, async ?? true, username ?? null, password ?? null);
128
+ };
129
+ XMLHttpRequest.prototype.send = function (body) {
130
+ this.addEventListener('load', function () {
131
+ if (self.isStrapiApiUrl(this._ollangUrl)) {
132
+ try {
133
+ const data = JSON.parse(this.responseText);
134
+ self.processStrapiResponse(this._ollangUrl, data);
135
+ }
136
+ catch (e) { }
137
+ }
138
+ });
139
+ return originalSend.call(this, body);
140
+ };
141
+ }
142
+ isStrapiApiUrl(url) {
143
+ if (!url)
144
+ return false;
145
+ if (this.config.strapiUrl && url.includes(this.config.strapiUrl)) {
146
+ return true;
147
+ }
148
+ const strapiPatterns = [/\/api\/([\w-]+)(\?|\/|$)/, /cms\./, /strapi/i];
149
+ return strapiPatterns.some((p) => p.test(url));
150
+ }
151
+ extractContentTypeFromUrl(url) {
152
+ const match = url.match(/\/api\/([\w-]+)/);
153
+ return match ? match[1] : null;
154
+ }
155
+ processStrapiResponse(url, responseData) {
156
+ const contentType = this.extractContentTypeFromUrl(url);
157
+ if (!contentType)
158
+ return;
159
+ // Track detected Strapi base URL
160
+ try {
161
+ const parsed = new URL(url, window.location.origin);
162
+ const base = parsed.origin !== window.location.origin ? parsed.origin : '';
163
+ if (base)
164
+ this.detectedStrapiUrls.add(base);
165
+ }
166
+ catch (e) { }
167
+ const data = responseData?.data;
168
+ if (!data)
169
+ return;
170
+ const entries = Array.isArray(data) ? data : [data];
171
+ for (const entry of entries) {
172
+ if (!entry || !entry.attributes)
173
+ continue;
174
+ const entryId = entry.id;
175
+ this.strapiEntries.set(`${contentType}:${entryId}`, {
176
+ contentType,
177
+ entryId,
178
+ attributes: entry.attributes,
179
+ url,
180
+ });
181
+ this.extractStrapiFields(entry.attributes, contentType, entryId, '');
182
+ }
183
+ console.log(`📦 Strapi [${contentType}]: captured ${entries.length} entries, total tracked: ${this.strapiContentMap.size}`);
184
+ }
185
+ extractStrapiFields(obj, contentType, entryId, fieldPath, depth = 0) {
186
+ if (!obj || typeof obj !== 'object')
187
+ return;
188
+ if (depth > OllangBrowser.MAX_RECURSION_DEPTH)
189
+ return;
190
+ for (const key of Object.keys(obj)) {
191
+ const value = obj[key];
192
+ const currentPath = fieldPath ? `${fieldPath}.${key}` : key;
193
+ // Skip relation fields that pull in unrelated entries
194
+ if (OllangBrowser.SKIP_RELATION_KEYS.has(key))
195
+ continue;
196
+ if (typeof value === 'string' && value.trim().length >= 2) {
197
+ if (this.isNonTranslatableField(key, value))
198
+ continue;
199
+ const normalized = this.normalizeText(value);
200
+ if (normalized.length < 2)
201
+ continue;
202
+ if (normalized.length > OllangBrowser.MAX_FIELD_LENGTH ||
203
+ OllangBrowser.LONG_TEXT_FIELDS.has(key)) {
204
+ const longKey = `${contentType}:${entryId}:${currentPath}`;
205
+ this.strapiLongTextMap.set(longKey, {
206
+ contentType,
207
+ entryId,
208
+ field: currentPath,
209
+ rawText: value,
210
+ });
211
+ this.extractParagraphsFromHtml(value, contentType, entryId, currentPath);
212
+ if (this.config.debug) {
213
+ console.log(`📝 Stored long field ${currentPath} (${normalized.length} chars) for API capture`);
214
+ }
215
+ continue;
216
+ }
217
+ this.strapiContentMap.set(normalized, {
218
+ contentType,
219
+ entryId,
220
+ field: currentPath,
221
+ rawText: value,
222
+ });
223
+ }
224
+ else if (Array.isArray(value)) {
225
+ value.forEach((item, idx) => {
226
+ if (typeof item === 'object' && item !== null) {
227
+ this.extractStrapiFields(item, contentType, entryId, `${currentPath}[${idx}]`, depth + 1);
228
+ }
229
+ else if (typeof item === 'string' && item.trim().length >= 2) {
230
+ const norm = this.normalizeText(item);
231
+ if (norm.length >= 2 && norm.length <= OllangBrowser.MAX_FIELD_LENGTH) {
232
+ this.strapiContentMap.set(norm, {
233
+ contentType,
234
+ entryId,
235
+ field: `${currentPath}[${idx}]`,
236
+ rawText: item,
237
+ });
238
+ }
239
+ }
240
+ });
241
+ }
242
+ else if (typeof value === 'object' && value !== null) {
243
+ if (key === 'formats' || key === 'provider_metadata')
244
+ continue;
245
+ if (this.isStrapiMediaObject(value)) {
246
+ this.extractStrapiMedia(value, contentType, entryId, currentPath);
247
+ continue;
248
+ }
249
+ if (key === 'data')
250
+ continue;
251
+ this.extractStrapiFields(value, contentType, entryId, currentPath, depth + 1);
252
+ }
253
+ }
254
+ }
255
+ isStrapiMediaObject(obj) {
256
+ if (!obj || typeof obj !== 'object')
257
+ return false;
258
+ const data = obj.data;
259
+ if (!data)
260
+ return false;
261
+ const single = Array.isArray(data) ? data[0] : data;
262
+ return !!(single?.attributes?.url && single?.attributes?.mime);
263
+ }
264
+ extractStrapiMedia(mediaObj, contentType, entryId, fieldPath) {
265
+ const strapiBase = this.config.strapiUrl || [...this.detectedStrapiUrls][0] || '';
266
+ const items = Array.isArray(mediaObj.data) ? mediaObj.data : [mediaObj.data];
267
+ for (const item of items) {
268
+ if (!item?.attributes?.url)
269
+ continue;
270
+ const attrs = item.attributes;
271
+ const rawUrl = attrs.url;
272
+ const mime = attrs.mime || '';
273
+ const isImage = mime.startsWith('image/') || /\.(jpg|jpeg|png|gif|svg|webp|avif)(\?|$)/i.test(rawUrl);
274
+ const isVideo = mime.startsWith('video/') || /\.(mp4|webm|ogg|mov)(\?|$)/i.test(rawUrl);
275
+ if (!isImage && !isVideo)
276
+ continue;
277
+ const absoluteUrl = rawUrl.startsWith('http') ? rawUrl : `${strapiBase}${rawUrl}`;
278
+ const meta = {
279
+ contentType,
280
+ entryId,
281
+ field: fieldPath,
282
+ url: absoluteUrl,
283
+ mime,
284
+ alt: attrs.alternativeText || attrs.caption || attrs.name || undefined,
285
+ };
286
+ this.strapiMediaMap.set(absoluteUrl, meta);
287
+ if (!rawUrl.startsWith('http')) {
288
+ this.strapiMediaMap.set(rawUrl, meta);
289
+ }
290
+ }
291
+ }
292
+ extractParagraphsFromHtml(html, contentType, entryId, fieldPath) {
293
+ const blocks = html
294
+ .split(/<\/p>|<br\s*\/?>|<\/h[1-6]>|<\/li>|<\/div>/i)
295
+ .map((block) => this.normalizeText(block))
296
+ .filter((block) => block.length >= 10);
297
+ for (const block of blocks) {
298
+ if (block.length > OllangBrowser.MAX_FIELD_LENGTH)
299
+ continue;
300
+ if (this.strapiContentMap.has(block))
301
+ continue;
302
+ this.strapiContentMap.set(block, {
303
+ contentType,
304
+ entryId,
305
+ field: fieldPath,
306
+ rawText: block,
307
+ });
308
+ }
309
+ }
310
+ isNonTranslatableField(key, value) {
311
+ if (OllangBrowser.SKIP_KEYS.has(key))
312
+ return true;
313
+ if (/^https?:\/\//.test(value))
314
+ return true;
315
+ if (/^\d{4}-\d{2}-\d{2}T/.test(value))
316
+ return true;
317
+ if (/^[a-f0-9]{16,}$/.test(value))
318
+ return true;
319
+ if (/^\.(jpg|jpeg|png|gif|svg|webp)$/i.test(value))
320
+ return true;
321
+ if (/^#[0-9a-fA-F]{3,8}$/.test(value))
322
+ return true;
323
+ if (key === 'fullName' || key === 'firstName' || key === 'lastName')
324
+ return true;
325
+ if (/^image\//.test(value) || /^video\//.test(value))
326
+ return true;
327
+ return false;
328
+ }
329
+ normalizeText(text) {
330
+ if (!text)
331
+ return '';
332
+ return text
333
+ .replace(/<[^>]*>/g, ' ')
334
+ .replace(/&nbsp;/g, ' ')
335
+ .replace(/&amp;/g, '&')
336
+ .replace(/&lt;/g, '<')
337
+ .replace(/&gt;/g, '>')
338
+ .replace(/&quot;/g, '"')
339
+ .replace(/&#39;/g, "'")
340
+ .replace(/\s+/g, ' ')
341
+ .trim();
342
+ }
343
+ findStrapiMatch(text) {
344
+ if (!text || this.strapiContentMap.size === 0)
345
+ return null;
346
+ const normalized = this.normalizeText(text);
347
+ if (normalized.length < 3)
348
+ return null;
349
+ const exact = this.strapiContentMap.get(normalized);
350
+ if (exact)
351
+ return exact;
352
+ let bestMatch = null;
353
+ let bestScore = 0;
354
+ for (const [strapiNorm, meta] of this.strapiContentMap) {
355
+ const domLen = normalized.length;
356
+ const strapiLen = strapiNorm.length;
357
+ if (strapiLen < 10 || domLen < 10)
358
+ continue;
359
+ if (domLen >= strapiLen && normalized.includes(strapiNorm)) {
360
+ const score = strapiLen / domLen;
361
+ if (score > bestScore && score > 0.6) {
362
+ bestScore = score;
363
+ bestMatch = meta;
364
+ }
365
+ }
366
+ if (strapiLen >= domLen && strapiNorm.includes(normalized)) {
367
+ const score = domLen / strapiLen;
368
+ if (score > bestScore && score > 0.5) {
369
+ bestScore = score;
370
+ bestMatch = meta;
371
+ }
372
+ }
373
+ if (domLen > 30 && strapiLen > 30) {
374
+ const checkLen = Math.min(domLen, strapiLen, 200);
375
+ const domPrefix = normalized.substring(0, checkLen);
376
+ const strapiPrefix = strapiNorm.substring(0, checkLen);
377
+ if (domPrefix === strapiPrefix) {
378
+ const score = checkLen / Math.max(domLen, strapiLen);
379
+ if (score > bestScore && score > 0.5) {
380
+ bestScore = score;
381
+ bestMatch = meta;
382
+ }
383
+ }
384
+ }
385
+ }
386
+ return bestMatch && bestScore > 0.5 ? bestMatch : null;
387
+ }
388
+ isI18nText(text) {
389
+ if (this.i18nTexts.has(text))
390
+ return true;
391
+ if (this.i18nTexts.has(text.trim()))
392
+ return true;
393
+ return this.i18nNormalized.has(this.normalizeText(text));
394
+ }
395
+ detectFrameworkI18n() {
396
+ setTimeout(() => {
397
+ try {
398
+ const translations = this.getAngularTranslations();
399
+ if (translations) {
400
+ this.extractTextsFromObject(translations);
401
+ console.log('✅ Loaded Angular translations from runtime');
402
+ }
403
+ }
404
+ catch (e) {
405
+ console.warn('Could not auto-detect Angular translations:', e);
406
+ }
407
+ }, 1500);
408
+ if (window.i18next) {
409
+ const i18n = window.i18next;
410
+ if (i18n.store && i18n.language) {
411
+ const translations = i18n.store.data[i18n.language];
412
+ if (translations) {
413
+ this.extractTextsFromObject(translations);
414
+ console.log('✅ Loaded React i18next translations');
415
+ }
416
+ }
417
+ }
418
+ if (window.__VUE_I18N__) {
419
+ const vueI18n = window.__VUE_I18N__;
420
+ if (vueI18n.messages) {
421
+ Object.values(vueI18n.messages).forEach((msgs) => {
422
+ this.extractTextsFromObject(msgs);
423
+ });
424
+ console.log('✅ Loaded Vue i18n translations');
425
+ }
426
+ }
427
+ if (window.__NEXT_DATA__?.props?.pageProps?.messages) {
428
+ this.extractTextsFromObject(window.__NEXT_DATA__.props.pageProps.messages);
429
+ console.log('✅ Loaded Next.js translations');
430
+ }
431
+ }
432
+ getAngularTranslations() {
433
+ if (window.ng && window.ng.probe) {
434
+ const appRoot = document.querySelector('app-root');
435
+ if (appRoot) {
436
+ try {
437
+ const ctx = window.ng.probe(appRoot);
438
+ if (ctx?.injector) {
439
+ const svc = ctx.injector.get('TranslateService');
440
+ if (svc?.translations)
441
+ return svc.translations;
442
+ }
443
+ }
444
+ catch (e) { }
445
+ }
446
+ }
447
+ if (window.__ANGULAR_TRANSLATIONS__) {
448
+ return window.__ANGULAR_TRANSLATIONS__;
449
+ }
450
+ try {
451
+ const stored = localStorage.getItem('translations') || sessionStorage.getItem('translations');
452
+ if (stored)
453
+ return JSON.parse(stored);
454
+ }
455
+ catch (e) { }
456
+ return null;
457
+ }
458
+ async loadI18nFiles() {
459
+ if (!this.config.i18nFiles || this.config.i18nFiles.length === 0) {
460
+ const paths = ['/assets/i18n/', '/locales/', '/i18n/', '/translations/', '/messages/'];
461
+ const langs = [
462
+ 'en',
463
+ 'tr',
464
+ 'de',
465
+ 'es',
466
+ 'fr',
467
+ 'it',
468
+ 'pt',
469
+ 'ru',
470
+ 'ja',
471
+ 'zh',
472
+ 'ko',
473
+ 'ar',
474
+ 'kr',
475
+ 'da',
476
+ 'fi',
477
+ 'nb',
478
+ 'nl',
479
+ 'sv',
480
+ ];
481
+ for (const basePath of paths) {
482
+ for (const lang of langs) {
483
+ const url = `${basePath}${lang}.json`;
484
+ try {
485
+ const response = await fetch(url);
486
+ if (response.ok) {
487
+ const data = await response.json();
488
+ this.extractTextsFromObject(data);
489
+ console.log(`✅ Auto-loaded i18n: ${url}`);
490
+ }
491
+ }
492
+ catch (error) { }
493
+ }
494
+ }
495
+ return;
496
+ }
497
+ for (const fileUrl of this.config.i18nFiles) {
498
+ try {
499
+ const response = await fetch(fileUrl);
500
+ const data = await response.json();
501
+ this.extractTextsFromObject(data);
502
+ }
503
+ catch (error) {
504
+ console.warn(`Failed to load i18n file: ${fileUrl}`, error);
505
+ }
506
+ }
507
+ }
508
+ extractTextsFromObject(obj) {
509
+ for (const key in obj) {
510
+ const value = obj[key];
511
+ if (typeof value === 'string') {
512
+ this.i18nTexts.add(value);
513
+ this.i18nTexts.add(value.trim());
514
+ this.i18nNormalized.add(this.normalizeText(value));
515
+ }
516
+ else if (typeof value === 'object' && value !== null) {
517
+ this.extractTextsFromObject(value);
518
+ }
519
+ }
520
+ }
521
+ startCapture() {
522
+ if (this.config.autoDetectCMS) {
523
+ this.detectCMS();
524
+ }
525
+ this.startObserving();
526
+ this.scanPage();
527
+ }
528
+ detectCMS() {
529
+ if (window.__CONTENTFUL_SPACE_ID__ || document.querySelector('[data-contentful-entry-id]')) {
530
+ this.config.cmsType = 'contentful';
531
+ }
532
+ if (window.__STRAPI__ || document.querySelector('[data-strapi-field]')) {
533
+ this.config.cmsType = 'strapi';
534
+ }
535
+ if (window.__SANITY__ || document.querySelector('[data-sanity]')) {
536
+ this.config.cmsType = 'sanity';
537
+ }
538
+ if (document.body.classList.contains('wordpress') || window.wp) {
539
+ this.config.cmsType = 'wordpress';
540
+ }
541
+ if (!this.config.cmsType && this.strapiContentMap.size > 0) {
542
+ this.config.cmsType = 'strapi';
543
+ }
544
+ if (this.detectedStrapiUrls.size > 0) {
545
+ this.config.cmsType = 'strapi';
546
+ if (!this.config.strapiUrl) {
547
+ this.config.strapiUrl = [...this.detectedStrapiUrls][0];
548
+ }
549
+ }
550
+ }
551
+ startObserving() {
552
+ this.observer = new MutationObserver((mutations) => {
553
+ this.handleMutations(mutations);
554
+ });
555
+ this.observer.observe(document.body, {
556
+ childList: true,
557
+ subtree: true,
558
+ characterData: true,
559
+ attributes: true,
560
+ attributeFilter: this.config.captureAttributes,
561
+ });
562
+ }
563
+ handleMutations(mutations) {
564
+ const affectedNodes = new Set();
565
+ mutations.forEach((m) => {
566
+ if (m.type === 'childList') {
567
+ m.addedNodes.forEach((n) => affectedNodes.add(n));
568
+ }
569
+ else if (m.type === 'characterData' || m.type === 'attributes') {
570
+ affectedNodes.add(m.target);
571
+ }
572
+ });
573
+ affectedNodes.forEach((node) => {
574
+ if (node.nodeType === Node.ELEMENT_NODE) {
575
+ const el = node;
576
+ this.scanElement(el);
577
+ if (el.querySelectorAll) {
578
+ this.config.captureSelectors.forEach((sel) => {
579
+ try {
580
+ el.querySelectorAll(sel).forEach((child) => this.scanElement(child));
581
+ }
582
+ catch (e) { }
583
+ });
584
+ }
585
+ }
586
+ });
587
+ }
588
+ captureApiContent() {
589
+ for (const [, meta] of this.strapiLongTextMap) {
590
+ const id = this.generateId(`api:${meta.contentType}:${meta.entryId}`, meta.field);
591
+ if (this.capturedContent.has(id))
592
+ continue;
593
+ const entryKey = `${meta.contentType}:${meta.entryId}`;
594
+ const entryData = this.strapiEntries.get(entryKey);
595
+ const route = entryData?.attributes?.route;
596
+ const content = {
597
+ id,
598
+ text: meta.rawText,
599
+ type: 'cms',
600
+ selector: `api://${meta.contentType}/${meta.entryId}/${meta.field}`,
601
+ xpath: '',
602
+ tagName: 'richtext',
603
+ attributes: {},
604
+ url: window.location.href,
605
+ timestamp: Date.now(),
606
+ cmsType: 'strapi',
607
+ cmsField: meta.field,
608
+ cmsId: String(meta.entryId),
609
+ strapiContentType: meta.contentType,
610
+ strapiEntryId: meta.entryId,
611
+ strapiField: meta.field,
612
+ strapiRoute: route,
613
+ };
614
+ this.capturedContent.set(id, content);
615
+ this.config.onContentDetected([content]);
616
+ }
617
+ for (const [, entryData] of this.strapiEntries) {
618
+ const route = entryData.attributes?.route;
619
+ if (!route)
620
+ continue;
621
+ for (const [, content] of this.capturedContent) {
622
+ if (content.strapiContentType === entryData.contentType &&
623
+ content.strapiEntryId === entryData.entryId &&
624
+ !content.strapiRoute) {
625
+ content.strapiRoute = route;
626
+ }
627
+ }
628
+ }
629
+ }
630
+ scanPage() {
631
+ this.config.captureSelectors.forEach((selector) => {
632
+ try {
633
+ document.querySelectorAll(selector).forEach((el) => this.scanElement(el));
634
+ }
635
+ catch (e) { }
636
+ });
637
+ this.scanMediaElements();
638
+ this.captureApiContent();
639
+ }
640
+ scanMediaElements() {
641
+ if (this.strapiMediaMap.size === 0)
642
+ return;
643
+ const mediaSelectors = ['img[src]', 'video[src]', 'source[src]', 'video[poster]'];
644
+ for (const sel of mediaSelectors) {
645
+ document.querySelectorAll(sel).forEach((el) => {
646
+ if (el.closest('#ollang-debug-panel'))
647
+ return;
648
+ const isPoster = sel.endsWith('[poster]');
649
+ const src = isPoster
650
+ ? el.poster
651
+ : el.src ||
652
+ el.getAttribute('src') ||
653
+ '';
654
+ if (!src)
655
+ return;
656
+ let meta = this.strapiMediaMap.get(src);
657
+ if (!meta) {
658
+ try {
659
+ const rel = new URL(src).pathname;
660
+ meta = this.strapiMediaMap.get(rel);
661
+ }
662
+ catch {
663
+ meta = this.strapiMediaMap.get(src);
664
+ }
665
+ }
666
+ if (!meta)
667
+ return;
668
+ const id = this.generateId(src, meta.contentType + meta.entryId);
669
+ if (this.capturedContent.has(id))
670
+ return;
671
+ const mime = meta.mime || '';
672
+ const mediaType = mime.startsWith('video/') || /\.(mp4|webm|ogg|mov)/i.test(src) ? 'video' : 'image';
673
+ const content = {
674
+ id,
675
+ text: meta.alt || meta.field,
676
+ type: 'cms',
677
+ selector: this.generateSelector(el),
678
+ xpath: this.generateXPath(el),
679
+ tagName: el.tagName.toLowerCase(),
680
+ attributes: this.extractAttributes(el),
681
+ url: window.location.href,
682
+ timestamp: Date.now(),
683
+ cmsType: 'strapi',
684
+ cmsField: meta.field,
685
+ cmsId: String(meta.entryId),
686
+ strapiContentType: meta.contentType,
687
+ strapiEntryId: meta.entryId,
688
+ strapiField: meta.field,
689
+ mediaUrl: meta.url,
690
+ mediaType,
691
+ mediaAlt: meta.alt,
692
+ };
693
+ this.capturedContent.set(id, content);
694
+ this.config.onContentDetected([content]);
695
+ });
696
+ }
697
+ }
698
+ scanElement(element) {
699
+ if (this.config.excludeSelectors.some((sel) => {
700
+ try {
701
+ return element.matches(sel);
702
+ }
703
+ catch (e) {
704
+ return false;
705
+ }
706
+ }))
707
+ return;
708
+ if (element.closest('#ollang-debug-panel'))
709
+ return;
710
+ const text = this.getDirectText(element);
711
+ if (!text || text.trim().length < 3)
712
+ return;
713
+ const trimmed = text.trim();
714
+ if (/^\d+$/.test(trimmed))
715
+ return;
716
+ if (trimmed.length < 5 && !trimmed.includes(' '))
717
+ return;
718
+ if (this.excludedTexts.has(trimmed))
719
+ return;
720
+ const normalizedForDup = this.normalizeText(trimmed);
721
+ if (this.capturedTexts.has(normalizedForDup))
722
+ return;
723
+ const strapiMatch = this.findStrapiMatch(trimmed);
724
+ if (strapiMatch) {
725
+ if (strapiMatch.field === 'description') {
726
+ if (this.config.debug) {
727
+ console.log(`ℹ️ Skipping DOM capture for long field ${strapiMatch.contentType}/${strapiMatch.entryId}/${strapiMatch.field}`);
728
+ }
729
+ return;
730
+ }
731
+ const captured = this.createCapturedContent(element, trimmed, strapiMatch);
732
+ if (!this.capturedContent.has(captured.id)) {
733
+ this.capturedContent.set(captured.id, captured);
734
+ this.capturedTexts.set(normalizedForDup, captured.id);
735
+ this.config.onContentDetected([captured]);
736
+ if (this.config.debug) {
737
+ console.log(`✅ CMS [${strapiMatch.contentType}/${strapiMatch.entryId}/${strapiMatch.field}]: "${trimmed.substring(0, 60)}..."`);
738
+ }
739
+ }
740
+ return;
741
+ }
742
+ const hasExplicitAttr = element.hasAttribute('data-cms') ||
743
+ element.hasAttribute('data-cms-field') ||
744
+ element.hasAttribute('data-strapi-field') ||
745
+ element.hasAttribute('data-strapi-component') ||
746
+ element.hasAttribute('data-contentful-entry-id') ||
747
+ element.hasAttribute('data-contentful-field-id') ||
748
+ element.hasAttribute('data-sanity') ||
749
+ element.hasAttribute('data-sanity-edit-target');
750
+ if (hasExplicitAttr) {
751
+ const captured = this.createCapturedContent(element, trimmed, null);
752
+ if (!this.capturedContent.has(captured.id)) {
753
+ this.capturedContent.set(captured.id, captured);
754
+ this.capturedTexts.set(normalizedForDup, captured.id);
755
+ this.config.onContentDetected([captured]);
756
+ }
757
+ return;
758
+ }
759
+ if (this.i18nTexts.size > 0 && this.strapiContentMap.size > 0 && !this.isI18nText(trimmed)) {
760
+ if (this.isLikelyStaticContent(element, trimmed))
761
+ return;
762
+ const captured = this.createCapturedContent(element, trimmed, null);
763
+ captured.type = 'dynamic-unmatched';
764
+ if (!this.capturedContent.has(captured.id)) {
765
+ this.capturedContent.set(captured.id, captured);
766
+ this.capturedTexts.set(normalizedForDup, captured.id);
767
+ this.config.onContentDetected([captured]);
768
+ }
769
+ }
770
+ }
771
+ getDirectText(element) {
772
+ let text = '';
773
+ for (const node of Array.from(element.childNodes)) {
774
+ if (node.nodeType === Node.TEXT_NODE) {
775
+ text += node.textContent;
776
+ }
777
+ }
778
+ if (text.trim())
779
+ return text.trim();
780
+ if (element.children.length === 0) {
781
+ return (element.textContent || '').trim();
782
+ }
783
+ return '';
784
+ }
785
+ isLikelyStaticContent(element, text) {
786
+ if (element.tagName === 'A' && text.length < 30)
787
+ return true;
788
+ if (element.tagName === 'BUTTON' && text.length < 30)
789
+ return true;
790
+ if (element.closest('footer') || element.closest('header, nav'))
791
+ return true;
792
+ if (/^(©|\||\+|→|←|×|✓|✗|▶|•)/.test(text))
793
+ return true;
794
+ if (/©\s*\d{4}/.test(text))
795
+ return true;
796
+ return false;
797
+ }
798
+ createCapturedContent(element, text, strapiMeta) {
799
+ const selector = this.generateSelector(element);
800
+ const xpath = this.generateXPath(element);
801
+ const cmsField = this.extractCMSField(element);
802
+ const cmsId = this.extractCMSId(element);
803
+ const content = {
804
+ id: this.generateId(selector, text),
805
+ text,
806
+ type: strapiMeta ? 'cms' : cmsField || cmsId ? 'cms' : 'dynamic',
807
+ selector,
808
+ xpath,
809
+ tagName: element.tagName.toLowerCase(),
810
+ attributes: this.extractAttributes(element),
811
+ url: window.location.href,
812
+ timestamp: Date.now(),
813
+ };
814
+ if (this.config.cmsType)
815
+ content.cmsType = this.config.cmsType;
816
+ if (strapiMeta) {
817
+ content.cmsType = 'strapi';
818
+ content.strapiContentType = strapiMeta.contentType;
819
+ content.strapiEntryId = strapiMeta.entryId;
820
+ content.strapiField = strapiMeta.field;
821
+ content.cmsField = strapiMeta.field;
822
+ content.cmsId = String(strapiMeta.entryId);
823
+ }
824
+ else {
825
+ if (cmsField)
826
+ content.cmsField = cmsField;
827
+ if (cmsId)
828
+ content.cmsId = cmsId;
829
+ }
830
+ return content;
831
+ }
832
+ extractCMSField(element) {
833
+ for (const attr of this.config.captureAttributes) {
834
+ const value = element.getAttribute(attr);
835
+ if (value)
836
+ return value;
837
+ }
838
+ return (element.getAttribute('data-cms-field') ||
839
+ element.getAttribute('data-contentful-field-id') ||
840
+ element.getAttribute('data-strapi-field') ||
841
+ element.getAttribute('data-sanity-edit-target') ||
842
+ undefined);
843
+ }
844
+ extractCMSId(element) {
845
+ return (element.getAttribute('data-cms-id') ||
846
+ element.getAttribute('data-contentful-entry-id') ||
847
+ element.getAttribute('data-strapi-id') ||
848
+ element.getAttribute('data-sanity-document-id') ||
849
+ undefined);
850
+ }
851
+ extractAttributes(element) {
852
+ const attrs = {};
853
+ Array.from(element.attributes).forEach((a) => {
854
+ if (a.name.startsWith('data-'))
855
+ attrs[a.name] = a.value;
856
+ });
857
+ return attrs;
858
+ }
859
+ generateSelector(element) {
860
+ if (element.id)
861
+ return `#${element.id}`;
862
+ const parts = [];
863
+ let el = element;
864
+ while (el && el !== document.body) {
865
+ let sel = el.tagName.toLowerCase();
866
+ if (el.className) {
867
+ const classes = Array.from(el.classList)
868
+ .filter((c) => !c.startsWith('ng-') && !c.startsWith('tw-'))
869
+ .slice(0, 2);
870
+ if (classes.length)
871
+ sel += '.' + classes.join('.');
872
+ }
873
+ parts.unshift(sel);
874
+ el = el.parentElement;
875
+ }
876
+ return parts.join(' > ');
877
+ }
878
+ generateXPath(element) {
879
+ if (element.id)
880
+ return `//*[@id="${element.id}"]`;
881
+ const parts = [];
882
+ let el = element;
883
+ while (el && el !== document.body) {
884
+ let idx = 1;
885
+ let sib = el.previousElementSibling;
886
+ while (sib) {
887
+ if (sib.tagName === el.tagName)
888
+ idx++;
889
+ sib = sib.previousElementSibling;
890
+ }
891
+ parts.unshift(`${el.tagName.toLowerCase()}[${idx}]`);
892
+ el = el.parentElement;
893
+ }
894
+ return '/' + parts.join('/');
895
+ }
896
+ generateId(selector, text) {
897
+ const str = selector + text;
898
+ let hash = 0;
899
+ for (let i = 0; i < str.length; i++) {
900
+ hash = (hash << 5) - hash + str.charCodeAt(i);
901
+ hash = hash & hash;
902
+ }
903
+ return Math.abs(hash).toString(36);
904
+ }
905
+ capture() {
906
+ this.scanPage();
907
+ this.groupCmsEntries();
908
+ return Array.from(this.capturedContent.values());
909
+ }
910
+ groupCmsEntries() {
911
+ const entryMap = new Map();
912
+ const nonCmsItems = [];
913
+ for (const [, item] of this.capturedContent) {
914
+ const isCmsEntry = item.strapiContentType && item.strapiEntryId != null;
915
+ const isMedia = !!item.mediaUrl;
916
+ if (isCmsEntry && !isMedia) {
917
+ const key = `${item.strapiContentType}:${item.strapiEntryId}`;
918
+ if (!entryMap.has(key)) {
919
+ entryMap.set(key, {
920
+ items: [],
921
+ entryData: this.strapiEntries.get(key),
922
+ });
923
+ }
924
+ entryMap.get(key).items.push(item);
925
+ }
926
+ else {
927
+ nonCmsItems.push(item);
928
+ }
929
+ }
930
+ for (const [key, group] of entryMap) {
931
+ const [contentType, entryIdStr] = key.split(':');
932
+ const entryId = Number(entryIdStr);
933
+ const hasDescription = group.items.some((i) => i.strapiField === 'description');
934
+ if (!hasDescription) {
935
+ const longKey = `${contentType}:${entryId}:description`;
936
+ const longMeta = this.strapiLongTextMap.get(longKey);
937
+ if (longMeta) {
938
+ group.items.push({
939
+ id: `api-desc-${contentType}-${entryId}`,
940
+ text: longMeta.rawText,
941
+ type: 'cms',
942
+ selector: `api://${contentType}/${entryId}/description`,
943
+ xpath: '',
944
+ tagName: 'richtext',
945
+ attributes: {},
946
+ url: window.location.href,
947
+ timestamp: Date.now(),
948
+ cmsType: 'strapi',
949
+ cmsField: 'description',
950
+ cmsId: String(entryId),
951
+ strapiContentType: contentType,
952
+ strapiEntryId: entryId,
953
+ strapiField: 'description',
954
+ });
955
+ }
956
+ }
957
+ }
958
+ this.capturedContent.clear();
959
+ for (const [key, group] of entryMap) {
960
+ const [contentType, entryIdStr] = key.split(':');
961
+ const entryId = Number(entryIdStr);
962
+ const titleItem = group.items.find((i) => i.strapiField?.includes('title')) || group.items[0];
963
+ const route = group.entryData?.attributes?.route ||
964
+ group.items.find((i) => i.strapiRoute)?.strapiRoute ||
965
+ undefined;
966
+ const cmsFields = {};
967
+ for (const item of group.items) {
968
+ if (item.strapiField) {
969
+ cmsFields[item.strapiField] = item.text;
970
+ }
971
+ }
972
+ const groupedId = `cms-entry-${contentType}-${entryId}`;
973
+ const fieldCount = Object.keys(cmsFields).length;
974
+ const grouped = {
975
+ ...titleItem,
976
+ id: groupedId,
977
+ text: titleItem.text,
978
+ tagName: `entry:${fieldCount} fields`,
979
+ strapiRoute: route,
980
+ cmsFields,
981
+ };
982
+ this.capturedContent.set(groupedId, grouped);
983
+ }
984
+ for (const item of nonCmsItems) {
985
+ this.capturedContent.set(item.id, item);
986
+ }
987
+ }
988
+ getCapturedContent() {
989
+ return Array.from(this.capturedContent.values());
990
+ }
991
+ getCmsContent() {
992
+ return Array.from(this.capturedContent.values()).filter((c) => c.type === 'cms');
993
+ }
994
+ getStrapiMetadata() {
995
+ return {
996
+ trackedTexts: this.strapiContentMap.size,
997
+ entries: this.strapiEntries.size,
998
+ detectedUrls: [...this.detectedStrapiUrls],
999
+ cmsType: this.config.cmsType,
1000
+ };
1001
+ }
1002
+ clear() {
1003
+ this.capturedContent.clear();
1004
+ this.capturedTexts.clear();
1005
+ }
1006
+ destroy() {
1007
+ if (this.observer) {
1008
+ this.observer.disconnect();
1009
+ this.observer = null;
1010
+ }
1011
+ const panel = document.getElementById('ollang-debug-panel');
1012
+ if (panel)
1013
+ panel.remove();
1014
+ }
1015
+ addI18nTexts(texts) {
1016
+ const before = this.i18nTexts.size;
1017
+ if (Array.isArray(texts)) {
1018
+ texts.forEach((t) => {
1019
+ this.i18nTexts.add(t);
1020
+ this.i18nTexts.add(t.trim());
1021
+ this.i18nNormalized.add(this.normalizeText(t));
1022
+ });
1023
+ }
1024
+ else {
1025
+ this.extractTextsFromObject(texts);
1026
+ }
1027
+ const added = this.i18nTexts.size - before;
1028
+ console.log(`✅ Added ${added} new i18n texts (total: ${this.i18nTexts.size})`);
1029
+ if (added > 0 && this.capturedContent.size > 0) {
1030
+ this.clear();
1031
+ this.scanPage();
1032
+ }
1033
+ }
1034
+ getI18nTextsCount() {
1035
+ return this.i18nTexts.size;
1036
+ }
1037
+ getEntryRoutes() {
1038
+ const routes = {};
1039
+ for (const [key, entry] of this.strapiEntries) {
1040
+ if (entry.attributes?.route) {
1041
+ routes[key] = entry.attributes.route;
1042
+ }
1043
+ }
1044
+ return routes;
1045
+ }
1046
+ async showDebugPanel() {
1047
+ if (document.getElementById('ollang-debug-panel'))
1048
+ return;
1049
+ const panel = this.createDebugPanel();
1050
+ document.body.appendChild(panel);
1051
+ if (!this.config.apiKey) {
1052
+ this.showApiKeyFormInPanel();
1053
+ return;
1054
+ }
1055
+ try {
1056
+ if (!(await this.validateApiKey())) {
1057
+ this.showApiKeyFormInPanel();
1058
+ return;
1059
+ }
1060
+ this.showPanelContent();
1061
+ }
1062
+ catch (e) {
1063
+ console.error('Failed to validate API key:', e);
1064
+ this.showApiKeyFormInPanel();
1065
+ }
1066
+ }
1067
+ async validateApiKey() {
1068
+ try {
1069
+ const baseUrl = (this.config.baseUrl || '').replace(/\/$/, '');
1070
+ if (!baseUrl)
1071
+ return false;
1072
+ const res = await fetch(`${baseUrl}/scans/folders`, {
1073
+ headers: { 'Content-Type': 'application/json', 'x-api-key': this.config.apiKey },
1074
+ });
1075
+ if (!res.ok)
1076
+ return false;
1077
+ const data = await res.json();
1078
+ const folders = Array.isArray(data) ? data : data.folders;
1079
+ if (folders && Array.isArray(folders)) {
1080
+ this.folders = folders;
1081
+ if (!this.selectedFolder && folders.length > 0)
1082
+ this.selectedFolder = folders[0].name;
1083
+ }
1084
+ return true;
1085
+ }
1086
+ catch (e) {
1087
+ return false;
1088
+ }
1089
+ }
1090
+ showApiKeyFormInPanel() {
1091
+ const container = document.getElementById('ollang-panel-content');
1092
+ if (!container)
1093
+ return;
1094
+ container.innerHTML = `
1095
+ <div style="padding: 20px;">
1096
+ <h3 style="margin: 0 0 15px 0; font-size: 16px; color: #333;">Ollang API Key Required</h3>
1097
+ <p style="margin: 0 0 20px 0; color: #666; font-size: 13px; line-height: 1.5;">
1098
+ Enter your Ollang API key for this app. Not the same as any Strapi token.
1099
+ </p>
1100
+ <div style="margin-bottom: 15px;">
1101
+ <label style="display: block; margin-bottom: 6px; font-weight: 500; color: #333; font-size: 13px;">Ollang API Key</label>
1102
+ <input type="text" id="ollang-apikey-input" placeholder="Ollang API key"
1103
+ style="width: 100%; padding: 8px 10px; border: 2px solid #ddd; border-radius: 4px; font-size: 13px; box-sizing: border-box; font-family: monospace;" />
1104
+ </div>
1105
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 15px;">
1106
+ <div id="ollang-status-indicator" style="width: 10px; height: 10px; border-radius: 50%; background: #ccc; transition: background-color 0.3s;"></div>
1107
+ <span id="ollang-status-text" style="font-size: 12px; color: #666;">Not validated</span>
1108
+ </div>
1109
+ <button id="ollang-validate-btn" class="ollang-btn" style="width: 100%;">Validate & Continue</button>
1110
+ </div>
1111
+ `;
1112
+ const input = document.getElementById('ollang-apikey-input');
1113
+ const btn = document.getElementById('ollang-validate-btn');
1114
+ const indicator = document.getElementById('ollang-status-indicator');
1115
+ const statusText = document.getElementById('ollang-status-text');
1116
+ const validate = async () => {
1117
+ const key = input.value.trim();
1118
+ if (!key) {
1119
+ this.showStatus('Please enter Ollang API key', 'error');
1120
+ return;
1121
+ }
1122
+ btn.disabled = true;
1123
+ btn.textContent = 'Validating...';
1124
+ indicator.style.background = '#ffc107';
1125
+ statusText.textContent = 'Validating...';
1126
+ statusText.style.color = '#ffc107';
1127
+ const prev = this.config.apiKey;
1128
+ this.config.apiKey = key;
1129
+ try {
1130
+ if (await this.validateApiKey()) {
1131
+ this.saveApiKey(key);
1132
+ indicator.style.background = '#28a745';
1133
+ statusText.textContent = 'Valid API key ✓';
1134
+ statusText.style.color = '#28a745';
1135
+ await new Promise((r) => setTimeout(r, 500));
1136
+ this.showPanelContent();
1137
+ }
1138
+ else {
1139
+ indicator.style.background = '#dc3545';
1140
+ statusText.textContent = 'Invalid API key ✗';
1141
+ statusText.style.color = '#dc3545';
1142
+ this.config.apiKey = prev;
1143
+ btn.disabled = false;
1144
+ btn.textContent = 'Validate & Continue';
1145
+ }
1146
+ }
1147
+ catch (e) {
1148
+ indicator.style.background = '#dc3545';
1149
+ statusText.textContent = 'Validation failed ✗';
1150
+ statusText.style.color = '#dc3545';
1151
+ this.config.apiKey = prev;
1152
+ btn.disabled = false;
1153
+ btn.textContent = 'Validate & Continue';
1154
+ }
1155
+ };
1156
+ btn.addEventListener('click', validate);
1157
+ input.addEventListener('keypress', (e) => {
1158
+ if (e.key === 'Enter')
1159
+ validate();
1160
+ });
1161
+ setTimeout(() => input.focus(), 100);
1162
+ }
1163
+ showPanelContent() {
1164
+ const container = document.getElementById('ollang-panel-content');
1165
+ if (!container)
1166
+ return;
1167
+ container.innerHTML = '';
1168
+ const stats = document.createElement('div');
1169
+ stats.id = 'ollang-stats';
1170
+ stats.style.cssText = 'padding: 15px; border-bottom: 1px solid #ddd; background: #f8f9fa;';
1171
+ this.updateStats(stats);
1172
+ const statusArea = document.createElement('div');
1173
+ statusArea.id = 'ollang-status';
1174
+ statusArea.style.cssText =
1175
+ 'padding: 10px 15px; border-bottom: 1px solid #ddd; background: #e7f3ff; font-size: 12px; display: none;';
1176
+ statusArea.innerHTML =
1177
+ '<span id="ollang-status-text">Ready</span><button id="ollang-status-close" style="float: right; background: none; border: none; cursor: pointer; font-size: 14px;">&times;</button>';
1178
+ const buttons = document.createElement('div');
1179
+ buttons.style.cssText =
1180
+ 'padding: 12px 16px; border-bottom: 1px solid #e2e8f0; display: flex; flex-direction: column; gap: 10px;';
1181
+ buttons.innerHTML = `
1182
+ <div style="display: flex; gap: 8px;">
1183
+ <button id="ollang-capture" class="ollang-btn">Capture</button>
1184
+ <button id="ollang-clear" class="ollang-btn ollang-btn-ghost">Clear</button>
1185
+ </div>
1186
+ <div style="display: flex; align-items: flex-end; justify-content: space-between; gap: 12px;">
1187
+ <div style="display: flex; flex-direction: column; gap: 4px; flex: 1;">
1188
+ <span style="font-size: 11px; font-weight: 500; color: #64748b;">Folder</span>
1189
+ <div style="display: flex; gap: 6px; align-items: center;">
1190
+ <div id="ollang-folder-dropdown" class="ollang-folder-dropdown">
1191
+ <button id="ollang-folder-trigger" type="button" class="ollang-folder-trigger">
1192
+ <span id="ollang-folder-label" class="ollang-folder-label">Select folder...</span>
1193
+ <span class="ollang-folder-arrow">▾</span>
1194
+ </button>
1195
+ <div id="ollang-folder-menu" class="ollang-folder-menu"></div>
1196
+ </div>
1197
+ <button id="ollang-new-folder" class="ollang-btn-sm">+ New</button>
1198
+ </div>
1199
+ </div>
1200
+ <button id="ollang-push-tms" class="ollang-btn ollang-btn-primary">Push to Ollang</button>
1201
+ </div>
1202
+ <div id="ollang-strapi-schema-block" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #e2e8f0;">
1203
+ <span style="font-size: 11px; font-weight: 500; color: #64748b;">Strapi schema (optional)</span>
1204
+ <p style="margin: 4px 0 8px 0; font-size: 11px; color: #94a3b8;">Fetch schema here so Push uses Content-Type Builder fields. Use your Strapi API token (not the Ollang TMS API token).</p>
1205
+ <div style="display: flex; flex-direction: column; gap: 6px;">
1206
+ <input type="text" id="ollang-strapi-url" placeholder="Strapi URL (e.g. https://api.example.com)" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; box-sizing: border-box;" />
1207
+ <input type="password" id="ollang-strapi-jwt" placeholder="Strapi Admin JWT token" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; box-sizing: border-box;" />
1208
+ <button id="ollang-fetch-schema" class="ollang-btn-sm">Fetch schema</button>
1209
+ </div>
1210
+ </div>
1211
+ `;
1212
+ const selectionInfo = document.createElement('div');
1213
+ selectionInfo.id = 'ollang-selection-info';
1214
+ selectionInfo.style.cssText =
1215
+ 'padding: 8px 16px; border-bottom: 1px solid #e2e8f0; background: #f8fafc; font-size: 12px; display: none;';
1216
+ selectionInfo.innerHTML = `
1217
+ <div class="ollang-selection-bar">
1218
+ <div class="ollang-selection-count">
1219
+ <span class="ollang-selection-dot"></span>
1220
+ <span id="ollang-selected-count">0</span>
1221
+ <span class="ollang-selection-label">items selected</span>
1222
+ </div>
1223
+ <div class="ollang-selection-actions">
1224
+ <button id="ollang-select-all" class="ollang-btn ollang-btn-ghost">Select All</button>
1225
+ <button id="ollang-deselect-all" class="ollang-btn ollang-btn-ghost">Deselect All</button>
1226
+ <button id="ollang-select-cms-only" class="ollang-btn ollang-selection-cms">Select CMS Only</button>
1227
+ </div>
1228
+ </div>
1229
+ `;
1230
+ const contentList = document.createElement('div');
1231
+ contentList.id = 'ollang-content-list';
1232
+ contentList.style.cssText = 'flex: 1; overflow-y: auto; padding: 15px;';
1233
+ this.loadFolders();
1234
+ container.appendChild(stats);
1235
+ container.appendChild(statusArea);
1236
+ container.appendChild(buttons);
1237
+ container.appendChild(selectionInfo);
1238
+ container.appendChild(contentList);
1239
+ setTimeout(() => {
1240
+ document.getElementById('ollang-capture')?.addEventListener('click', () => {
1241
+ this.capture();
1242
+ this.updateStats(stats);
1243
+ this.showContent(contentList);
1244
+ this.showStatus(`Captured ${this.capturedContent.size} items (${this.getCmsContent().length} CMS)`, 'success');
1245
+ });
1246
+ document.getElementById('ollang-clear')?.addEventListener('click', () => {
1247
+ this.clear();
1248
+ this.updateStats(stats);
1249
+ contentList.innerHTML =
1250
+ '<p style="color: #999; text-align: center;">No content captured</p>';
1251
+ this.showStatus('Cleared all content', 'success');
1252
+ });
1253
+ document.getElementById('ollang-select-all')?.addEventListener('click', () => {
1254
+ Array.from(this.capturedContent.values()).forEach((c) => this.selectedContentIds.add(c.id));
1255
+ this.showContent(contentList);
1256
+ });
1257
+ document.getElementById('ollang-deselect-all')?.addEventListener('click', () => {
1258
+ this.selectedContentIds.clear();
1259
+ this.showContent(contentList);
1260
+ });
1261
+ document.getElementById('ollang-select-cms-only')?.addEventListener('click', () => {
1262
+ this.selectedContentIds.clear();
1263
+ this.getCmsContent().forEach((c) => this.selectedContentIds.add(c.id));
1264
+ this.showContent(contentList);
1265
+ });
1266
+ document.getElementById('ollang-push-tms')?.addEventListener('click', () => this.pushToTMS());
1267
+ document
1268
+ .getElementById('ollang-fetch-schema')
1269
+ ?.addEventListener('click', () => this.fetchStrapiSchemaInPanel());
1270
+ const strapiUrlInput = document.getElementById('ollang-strapi-url');
1271
+ if (strapiUrlInput && !strapiUrlInput.value) {
1272
+ strapiUrlInput.value = this.config.strapiUrl || [...this.detectedStrapiUrls][0] || '';
1273
+ }
1274
+ this.updateFolderOptions();
1275
+ const dropdown = document.getElementById('ollang-folder-dropdown');
1276
+ const trigger = document.getElementById('ollang-folder-trigger');
1277
+ const menu = document.getElementById('ollang-folder-menu');
1278
+ if (trigger && menu && dropdown) {
1279
+ const toggleMenu = (open) => {
1280
+ const isOpen = open ?? menu.getAttribute('data-open') !== 'true';
1281
+ if (isOpen) {
1282
+ menu.style.display = 'block';
1283
+ menu.setAttribute('data-open', 'true');
1284
+ }
1285
+ else {
1286
+ menu.style.display = 'none';
1287
+ menu.setAttribute('data-open', 'false');
1288
+ }
1289
+ };
1290
+ trigger.addEventListener('click', (e) => {
1291
+ e.stopPropagation();
1292
+ toggleMenu();
1293
+ });
1294
+ document.addEventListener('click', (e) => {
1295
+ if (!dropdown.contains(e.target)) {
1296
+ toggleMenu(false);
1297
+ }
1298
+ });
1299
+ }
1300
+ document
1301
+ .getElementById('ollang-new-folder')
1302
+ ?.addEventListener('click', () => this.showNewFolderDialog());
1303
+ document
1304
+ .getElementById('ollang-status-close')
1305
+ ?.addEventListener('click', () => this.hideStatus());
1306
+ }, 0);
1307
+ setInterval(() => this.updateStats(stats), 2000);
1308
+ }
1309
+ createDebugPanel() {
1310
+ const panel = document.createElement('div');
1311
+ panel.id = 'ollang-debug-panel';
1312
+ panel.style.cssText =
1313
+ [
1314
+ 'position: fixed',
1315
+ 'right: 20px',
1316
+ 'left: auto',
1317
+ 'transform: none',
1318
+ 'bottom: 0',
1319
+ 'width: min(540px, 100% - 40px)',
1320
+ 'min-height: 320px',
1321
+ 'max-height: 90vh',
1322
+ 'background: #ffffff',
1323
+ 'border-radius: 12px 12px 0 0',
1324
+ 'border: 1px solid rgba(15, 23, 42, 0.12)',
1325
+ 'box-shadow: 0 18px 45px rgba(15, 23, 42, 0.25)',
1326
+ 'z-index: 999999',
1327
+ 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1328
+ 'display: flex',
1329
+ 'flex-direction: column',
1330
+ 'overflow: hidden',
1331
+ 'backdrop-filter: blur(10px)',
1332
+ '-webkit-backdrop-filter: blur(10px)',
1333
+ 'background-clip: padding-box',
1334
+ ].join('; ') + ';';
1335
+ // Resize handle so user can drag panel upwards
1336
+ const resizer = document.createElement('div');
1337
+ resizer.style.cssText =
1338
+ [
1339
+ 'height: 6px',
1340
+ 'cursor: ns-resize',
1341
+ 'display: flex',
1342
+ 'align-items: center',
1343
+ 'justify-content: center',
1344
+ 'background: transparent',
1345
+ ].join('; ') + ';';
1346
+ const resizerBar = document.createElement('div');
1347
+ resizerBar.style.cssText =
1348
+ 'width: 36px; height: 3px; border-radius: 999px; background: rgba(148, 163, 184, 0.95);';
1349
+ resizer.appendChild(resizerBar);
1350
+ let isResizing = false;
1351
+ let startY = 0;
1352
+ let startHeight = 0;
1353
+ const onMouseMove = (e) => {
1354
+ if (!isResizing)
1355
+ return;
1356
+ const delta = startY - e.clientY;
1357
+ const newHeight = Math.min(Math.max(startHeight + delta, 140), Math.round(window.innerHeight * 0.7));
1358
+ panel.style.height = `${newHeight}px`;
1359
+ };
1360
+ const stopResize = () => {
1361
+ if (!isResizing)
1362
+ return;
1363
+ isResizing = false;
1364
+ document.removeEventListener('mousemove', onMouseMove);
1365
+ document.removeEventListener('mouseup', stopResize);
1366
+ };
1367
+ resizer.addEventListener('mousedown', (e) => {
1368
+ isResizing = true;
1369
+ startY = e.clientY;
1370
+ startHeight = panel.getBoundingClientRect().height;
1371
+ document.addEventListener('mousemove', onMouseMove);
1372
+ document.addEventListener('mouseup', stopResize);
1373
+ });
1374
+ const header = document.createElement('div');
1375
+ header.style.cssText =
1376
+ [
1377
+ 'padding: 10px 16px',
1378
+ 'border-bottom: 1px solid rgba(148, 163, 184, 0.25)',
1379
+ 'display: flex',
1380
+ 'justify-content: space-between',
1381
+ 'align-items: center',
1382
+ 'background: #ffffff',
1383
+ 'border-radius: 12px 12px 0 0',
1384
+ 'color: #0f172a',
1385
+ 'box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04)',
1386
+ ].join('; ') + ';';
1387
+ header.innerHTML = `
1388
+ <div style="display: flex; align-items: center; gap: 10px;">
1389
+ <div style="width: 32px; height: 32px; border-radius: 999px; background: #ffffff; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.45); padding: 4px;">
1390
+ <svg viewBox="0 0 37 32" xmlns="http://www.w3.org/2000/svg" style="width: 26px; height: 22px; display: block;">
1391
+ <path d="M35.8246 10.862C34.5972 11.5165 33.2999 12.0249 31.9585 12.3772C30.4527 5.10884 24.8838 0.0517578 18.3428 0.0517578H18.2347C15.3756 0.184498 13.2599 1.58635 12.4149 3.89149C11.2871 6.96393 12.7167 11.1501 15.9666 14.3132C18.6573 16.9259 22.7585 18.4605 26.9677 18.4378C26.2857 21.1303 24.6634 23.4766 22.405 25.037C20.1466 26.5973 17.4072 27.2645 14.7005 26.9134C11.9939 26.5622 9.50584 25.2168 7.70306 23.1296C5.90027 21.0423 4.90653 18.3565 4.90817 15.5759C4.90817 12.9858 6.04543 9.13633 9.25081 6.75996L9.56849 6.52687V0.699269L9.28261 0.854665C8.27975 1.42954 7.30632 2.0563 6.36626 2.73246C1.67098 6.21284 0.0126953 11.6552 0.0126953 15.592C0.0174583 19.7867 1.59692 23.8205 4.427 26.8662C7.25707 29.9119 11.1233 31.7386 15.2329 31.9718C19.3424 32.2049 23.3837 30.8267 26.528 28.12C29.6723 25.4132 31.6812 21.583 32.1427 17.4148C32.5049 17.282 33.0036 17.1428 33.5278 16.9939C34.4967 16.7187 35.4973 16.4338 36.0247 16.1133L36.1168 16.0583V10.7325L35.8246 10.862ZM27.1297 13.4326C24.7312 13.4746 21.4972 12.7851 19.3529 10.6968C17.504 8.89676 16.6495 6.63372 17.0085 5.64626C17.1705 5.21243 17.8598 5.08294 18.3999 5.05056C21.9642 5.0797 26.1639 8.21686 27.1297 13.4326Z" fill="#6148f9" />
1392
+ </svg>
1393
+ </div>
1394
+ <div style="display: flex; flex-direction: column;">
1395
+ <span style="font-size: 13px; font-weight: 600; color: #0f172a; letter-spacing: 0.02em;">Ollang</span>
1396
+ <span style="font-size: 11px; font-weight: 500; color: #64748b;">CMS Detect</span>
1397
+ </div>
1398
+ </div>
1399
+ <button id="ollang-close"
1400
+ style="background: #f8fafc; border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.6); width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; color: #0f172a; cursor: pointer; font-size: 18px; line-height: 1; padding: 0;">
1401
+ ×
1402
+ </button>
1403
+ `;
1404
+ const content = document.createElement('div');
1405
+ content.id = 'ollang-panel-content';
1406
+ content.style.cssText = 'flex: 1; overflow-y: auto; display: flex; flex-direction: column;';
1407
+ const style = document.createElement('style');
1408
+ style.textContent = `
1409
+ .ollang-btn {
1410
+ padding: 6px 12px;
1411
+ border-radius: 6px;
1412
+ border: 1px solid #e2e8f0;
1413
+ background: #ffffff;
1414
+ color: #0f172a;
1415
+ cursor: pointer;
1416
+ font-size: 12px;
1417
+ font-weight: 500;
1418
+ display: inline-flex;
1419
+ align-items: center;
1420
+ justify-content: center;
1421
+ gap: 4px;
1422
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
1423
+ }
1424
+ .ollang-btn:hover {
1425
+ background: #f8fafc;
1426
+ border-color: #cbd5f5;
1427
+ }
1428
+ .ollang-btn:disabled {
1429
+ background: #f8fafc;
1430
+ border-color: #e2e8f0;
1431
+ color: #94a3b8;
1432
+ cursor: not-allowed;
1433
+ box-shadow: none;
1434
+ }
1435
+ .ollang-btn-primary {
1436
+ background: #1d4ed8;
1437
+ border-color: #1d4ed8;
1438
+ color: #ffffff;
1439
+ }
1440
+ .ollang-btn-primary:hover {
1441
+ background: #1e40af;
1442
+ border-color: #1e40af;
1443
+ }
1444
+ .ollang-btn-ghost {
1445
+ background: #f8fafc;
1446
+ border-color: #e2e8f0;
1447
+ color: #0f172a;
1448
+ }
1449
+ .ollang-btn-link {
1450
+ background: none;
1451
+ border: none;
1452
+ color: #2563eb;
1453
+ cursor: pointer;
1454
+ font-size: 11px;
1455
+ text-decoration: underline;
1456
+ padding: 0;
1457
+ }
1458
+ .ollang-btn-sm {
1459
+ padding: 4px 10px;
1460
+ background: #0f172a;
1461
+ color: #ffffff;
1462
+ border-radius: 999px;
1463
+ border: none;
1464
+ cursor: pointer;
1465
+ font-size: 11px;
1466
+ font-weight: 500;
1467
+ }
1468
+ .ollang-btn-sm:hover {
1469
+ background: #020617;
1470
+ }
1471
+ .ollang-selection-bar {
1472
+ display: flex;
1473
+ align-items: center;
1474
+ justify-content: space-between;
1475
+ gap: 8px;
1476
+ }
1477
+ .ollang-selection-count {
1478
+ display: inline-flex;
1479
+ align-items: center;
1480
+ gap: 6px;
1481
+ color: #0f172a;
1482
+ }
1483
+ .ollang-selection-dot {
1484
+ width: 8px;
1485
+ height: 8px;
1486
+ border-radius: 999px;
1487
+ background: #22c55e;
1488
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.25);
1489
+ }
1490
+ .ollang-selection-label {
1491
+ font-size: 11px;
1492
+ color: #64748b;
1493
+ }
1494
+ .ollang-selection-actions {
1495
+ display: inline-flex;
1496
+ align-items: center;
1497
+ gap: 8px;
1498
+ }
1499
+ .ollang-selection-cms {
1500
+ color: #16a34a;
1501
+ }
1502
+ .ollang-folder-dropdown {
1503
+ position: relative;
1504
+ min-width: 220px;
1505
+ }
1506
+ .ollang-folder-trigger {
1507
+ width: 100%;
1508
+ padding: 6px 10px;
1509
+ border-radius: 999px;
1510
+ border: 1px solid #e2e8f0;
1511
+ background: #ffffff;
1512
+ color: #0f172a;
1513
+ font-size: 12px;
1514
+ display: flex;
1515
+ align-items: center;
1516
+ justify-content: space-between;
1517
+ cursor: pointer;
1518
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
1519
+ }
1520
+ .ollang-folder-trigger:hover {
1521
+ border-color: #cbd5f5;
1522
+ background: #f8fafc;
1523
+ }
1524
+ .ollang-folder-label {
1525
+ overflow: hidden;
1526
+ text-overflow: ellipsis;
1527
+ white-space: nowrap;
1528
+ }
1529
+ .ollang-folder-arrow {
1530
+ font-size: 10px;
1531
+ color: #94a3b8;
1532
+ margin-left: 6px;
1533
+ }
1534
+ .ollang-folder-menu {
1535
+ position: absolute;
1536
+ top: calc(100% + 4px);
1537
+ left: 0;
1538
+ right: 0;
1539
+ max-height: 200px;
1540
+ overflow-y: auto;
1541
+ background: #ffffff;
1542
+ border-radius: 12px;
1543
+ border: 1px solid #e2e8f0;
1544
+ box-shadow: 0 10px 25px rgba(15, 23, 42, 0.15);
1545
+ padding: 4px;
1546
+ display: none;
1547
+ z-index: 10;
1548
+ }
1549
+ .ollang-folder-option {
1550
+ width: 100%;
1551
+ text-align: left;
1552
+ padding: 6px 8px;
1553
+ border-radius: 8px;
1554
+ border: none;
1555
+ background: transparent;
1556
+ font-size: 12px;
1557
+ color: #0f172a;
1558
+ cursor: pointer;
1559
+ }
1560
+ .ollang-folder-option:hover {
1561
+ background: #eff6ff;
1562
+ }
1563
+ .ollang-folder-option-active {
1564
+ background: #1d4ed8;
1565
+ color: #ffffff;
1566
+ }
1567
+ .ollang-content-item {
1568
+ background: #f8fafc;
1569
+ padding: 10px;
1570
+ margin: 6px 0;
1571
+ border-radius: 8px;
1572
+ font-size: 12px;
1573
+ border: 1px solid #e2e8f0;
1574
+ display: flex;
1575
+ gap: 10px;
1576
+ align-items: flex-start;
1577
+ }
1578
+ .ollang-content-item.cms-matched {
1579
+ border-color: #22c55e;
1580
+ background: #f0fdf4;
1581
+ }
1582
+ .ollang-content-item.selected {
1583
+ background: #eff6ff;
1584
+ border-color: #2563eb;
1585
+ }
1586
+ .ollang-content-checkbox {
1587
+ margin-top: 2px;
1588
+ cursor: pointer;
1589
+ width: 16px;
1590
+ height: 16px;
1591
+ }
1592
+ .ollang-content-body {
1593
+ flex: 1;
1594
+ }
1595
+ .ollang-content-text {
1596
+ font-weight: 600;
1597
+ margin-bottom: 5px;
1598
+ color: #0f172a;
1599
+ }
1600
+ .ollang-content-meta {
1601
+ color: #64748b;
1602
+ font-size: 11px;
1603
+ }
1604
+ .ollang-cms-badge {
1605
+ display: inline-block;
1606
+ padding: 1px 6px;
1607
+ border-radius: 999px;
1608
+ font-size: 10px;
1609
+ font-weight: 600;
1610
+ }
1611
+ .ollang-badge-cms {
1612
+ background: #dcfce7;
1613
+ color: #15803d;
1614
+ }
1615
+ .ollang-badge-dynamic {
1616
+ background: #fef9c3;
1617
+ color: #854d0e;
1618
+ }
1619
+ .ollang-badge-unmatched {
1620
+ background: #fee2e2;
1621
+ color: #b91c1c;
1622
+ }
1623
+ #ollang-apikey-input:focus {
1624
+ outline: none;
1625
+ border-color: #2563eb;
1626
+ }
1627
+ .ollang-badge-image {
1628
+ background: #ede9fe;
1629
+ color: #6d28d9;
1630
+ }
1631
+ .ollang-badge-video {
1632
+ background: #fce7f3;
1633
+ color: #be185d;
1634
+ }
1635
+ .ollang-media-item {
1636
+ align-items: center;
1637
+ }
1638
+ .ollang-media-preview {
1639
+ width: 52px;
1640
+ height: 40px;
1641
+ border-radius: 6px;
1642
+ object-fit: cover;
1643
+ flex-shrink: 0;
1644
+ border: 1px solid #e2e8f0;
1645
+ background: #f1f5f9;
1646
+ }
1647
+ .ollang-media-video-thumb {
1648
+ width: 52px;
1649
+ height: 40px;
1650
+ border-radius: 6px;
1651
+ background: #0f172a;
1652
+ display: flex;
1653
+ align-items: center;
1654
+ justify-content: center;
1655
+ flex-shrink: 0;
1656
+ border: 1px solid #e2e8f0;
1657
+ }
1658
+ .ollang-media-play {
1659
+ color: #ffffff;
1660
+ font-size: 14px;
1661
+ opacity: 0.9;
1662
+ }
1663
+ `;
1664
+ panel.appendChild(style);
1665
+ panel.appendChild(resizer);
1666
+ panel.appendChild(header);
1667
+ panel.appendChild(content);
1668
+ setTimeout(() => {
1669
+ document.getElementById('ollang-close')?.addEventListener('click', () => panel.remove());
1670
+ }, 0);
1671
+ return panel;
1672
+ }
1673
+ updateStats(el) {
1674
+ if (!el)
1675
+ return;
1676
+ const total = this.capturedContent.size;
1677
+ const cms = this.getCmsContent().length;
1678
+ const mediaItems = Array.from(this.capturedContent.values()).filter((c) => !!c.mediaUrl);
1679
+ const imgCount = mediaItems.filter((c) => c.mediaType === 'image').length;
1680
+ const vidCount = mediaItems.filter((c) => c.mediaType === 'video').length;
1681
+ el.innerHTML = `
1682
+ <div style="display: flex; flex-direction: column; gap: 2px;">
1683
+ <div style="font-size: 12px; font-weight: 600; color: #0f172a;">
1684
+ ${total} captured
1685
+ <span style="font-weight: 400; color: #64748b; margin-left: 4px;">(${cms} text · ${imgCount} image · ${vidCount} video)</span>
1686
+ </div>
1687
+ <div style="font-size: 11px; color: #94a3b8;">
1688
+ ${this.config.cmsType || 'Auto-detect'}${this.config.strapiUrl ? ' · ' + this.config.strapiUrl : ''}
1689
+ · tracked: ${this.strapiContentMap.size} texts, ${this.strapiMediaMap.size} media
1690
+ </div>
1691
+ </div>
1692
+ `;
1693
+ }
1694
+ showContent(listDiv) {
1695
+ const items = Array.from(this.capturedContent.values());
1696
+ if (items.length === 0) {
1697
+ listDiv.innerHTML = '<p style="color: #999; text-align: center;">No content captured yet</p>';
1698
+ this.updateSelectionInfo();
1699
+ return;
1700
+ }
1701
+ items.sort((a, b) => {
1702
+ if (a.type === 'cms' && b.type !== 'cms')
1703
+ return -1;
1704
+ if (a.type !== 'cms' && b.type === 'cms')
1705
+ return 1;
1706
+ return b.text.length - a.text.length;
1707
+ });
1708
+ listDiv.innerHTML = items
1709
+ .map((item) => {
1710
+ const sel = this.selectedContentIds.has(item.id);
1711
+ const isCms = item.type === 'cms';
1712
+ const bc = isCms
1713
+ ? 'ollang-badge-cms'
1714
+ : item.type === 'dynamic-unmatched'
1715
+ ? 'ollang-badge-unmatched'
1716
+ : 'ollang-badge-dynamic';
1717
+ const bl = isCms ? 'CMS' : item.type === 'dynamic-unmatched' ? 'Unmatched' : 'Dynamic';
1718
+ // Media item rendering
1719
+ if (item.mediaUrl) {
1720
+ const isVideo = item.mediaType === 'video';
1721
+ const mediaBadgeLabel = isVideo ? 'Video' : 'Image';
1722
+ const mediaBadgeClass = isVideo ? 'ollang-badge-video' : 'ollang-badge-image';
1723
+ const preview = isVideo
1724
+ ? `<div class="ollang-media-preview ollang-media-video-thumb">
1725
+ <span class="ollang-media-play">▶</span>
1726
+ </div>`
1727
+ : `<img class="ollang-media-preview" src="${this.escapeHtml(item.mediaUrl)}" alt="${this.escapeHtml(item.mediaAlt || '')}" loading="lazy" />`;
1728
+ return `<div class="ollang-content-item ${isCms ? 'cms-matched' : ''} ollang-media-item ${sel ? 'selected' : ''}" data-id="${item.id}">
1729
+ <input type="checkbox" class="ollang-content-checkbox" data-id="${item.id}" ${sel ? 'checked' : ''}>
1730
+ ${preview}
1731
+ <div class="ollang-content-body">
1732
+ <div class="ollang-content-text">${this.escapeHtml((item.mediaAlt || item.strapiField || item.mediaUrl).substring(0, 80))}</div>
1733
+ <div class="ollang-content-meta">
1734
+ <span class="ollang-cms-badge ollang-badge-cms">${bl}</span>
1735
+ <span class="ollang-cms-badge ${mediaBadgeClass}">${mediaBadgeLabel}</span>
1736
+ ${item.strapiContentType ? ' ' + item.strapiContentType : ''}${item.strapiEntryId ? '#' + item.strapiEntryId : ''}${item.strapiField ? ' → ' + item.strapiField : ''}
1737
+ </div>
1738
+ </div>
1739
+ </div>`;
1740
+ }
1741
+ // Text item rendering
1742
+ const isEntry = !!item.cmsFields;
1743
+ const entryLabel = isEntry
1744
+ ? `<strong>${item.strapiContentType}#${item.strapiEntryId}</strong> (${Object.keys(item.cmsFields).join(', ')})`
1745
+ : `&lt;${item.tagName}&gt;${item.strapiContentType ? ' | ' + item.strapiContentType : ''}${item.strapiEntryId ? '#' + item.strapiEntryId : ''}${item.strapiField ? ' → ' + item.strapiField : ''}`;
1746
+ return `<div class="ollang-content-item ${isCms ? 'cms-matched' : ''} ${sel ? 'selected' : ''}" data-id="${item.id}">
1747
+ <input type="checkbox" class="ollang-content-checkbox" data-id="${item.id}" ${sel ? 'checked' : ''}>
1748
+ <div class="ollang-content-body">
1749
+ <div class="ollang-content-text">${this.escapeHtml(item.text.substring(0, 80))}${item.text.length > 80 ? '...' : ''}</div>
1750
+ <div class="ollang-content-meta"><span class="ollang-cms-badge ${bc}">${bl}</span> ${entryLabel}</div>
1751
+ </div></div>`;
1752
+ })
1753
+ .join('');
1754
+ listDiv.querySelectorAll('.ollang-content-checkbox').forEach((cb) => {
1755
+ cb.addEventListener('change', (e) => {
1756
+ const t = e.target;
1757
+ const id = t.dataset.id;
1758
+ const row = listDiv.querySelector(`[data-id="${id}"]`);
1759
+ if (t.checked) {
1760
+ this.selectedContentIds.add(id);
1761
+ row?.classList.add('selected');
1762
+ }
1763
+ else {
1764
+ this.selectedContentIds.delete(id);
1765
+ row?.classList.remove('selected');
1766
+ }
1767
+ this.updateSelectionInfo();
1768
+ });
1769
+ });
1770
+ this.updateSelectionInfo();
1771
+ }
1772
+ updateSelectionInfo() {
1773
+ const info = document.getElementById('ollang-selection-info');
1774
+ const count = document.getElementById('ollang-selected-count');
1775
+ const pushBtn = document.getElementById('ollang-push-tms');
1776
+ if (info && count) {
1777
+ if (this.selectedContentIds.size > 0) {
1778
+ info.style.display = 'block';
1779
+ count.textContent = String(this.selectedContentIds.size);
1780
+ }
1781
+ else
1782
+ info.style.display = 'none';
1783
+ }
1784
+ if (pushBtn)
1785
+ pushBtn.disabled = this.selectedContentIds.size === 0;
1786
+ }
1787
+ exportContent() {
1788
+ const data = JSON.stringify(Array.from(this.capturedContent.values()), null, 2);
1789
+ const blob = new Blob([data], { type: 'application/json' });
1790
+ const url = URL.createObjectURL(blob);
1791
+ const a = document.createElement('a');
1792
+ a.href = url;
1793
+ a.download = `ollang-captured-${Date.now()}.json`;
1794
+ a.click();
1795
+ URL.revokeObjectURL(url);
1796
+ }
1797
+ escapeHtml(text) {
1798
+ const div = document.createElement('div');
1799
+ div.textContent = text;
1800
+ return div.innerHTML;
1801
+ }
1802
+ showStatus(message, type = 'info') {
1803
+ const area = document.getElementById('ollang-status');
1804
+ const text = document.getElementById('ollang-status-text');
1805
+ if (area && text) {
1806
+ text.textContent = message;
1807
+ area.style.backgroundColor = { success: '#d4edda', error: '#f8d7da', info: '#e7f3ff' }[type];
1808
+ area.style.display = 'block';
1809
+ if (type !== 'error')
1810
+ setTimeout(() => {
1811
+ if (area)
1812
+ area.style.display = 'none';
1813
+ }, 5000);
1814
+ }
1815
+ }
1816
+ hideStatus() {
1817
+ const el = document.getElementById('ollang-status');
1818
+ if (el)
1819
+ el.style.display = 'none';
1820
+ }
1821
+ async loadFolders() {
1822
+ try {
1823
+ if (this.folders.length > 0) {
1824
+ this.updateFolderOptions();
1825
+ return;
1826
+ }
1827
+ if (!this.config.apiKey)
1828
+ return;
1829
+ const baseUrl = (this.config.baseUrl || '').replace(/\/$/, '');
1830
+ if (!baseUrl)
1831
+ return;
1832
+ const res = await fetch(`${baseUrl}/scans/folders`, {
1833
+ headers: { 'Content-Type': 'application/json', 'x-api-key': this.config.apiKey },
1834
+ });
1835
+ if (res.ok) {
1836
+ const data = await res.json();
1837
+ const folders = Array.isArray(data) ? data : data.folders;
1838
+ if (folders?.length > 0) {
1839
+ this.folders = folders;
1840
+ if (!this.selectedFolder)
1841
+ this.selectedFolder = folders[0].name;
1842
+ this.updateFolderOptions();
1843
+ }
1844
+ }
1845
+ }
1846
+ catch (e) {
1847
+ console.warn('Failed to load folders:', e);
1848
+ }
1849
+ }
1850
+ updateFolderOptions() {
1851
+ const label = document.getElementById('ollang-folder-label');
1852
+ const menu = document.getElementById('ollang-folder-menu');
1853
+ if (!label || !menu)
1854
+ return;
1855
+ // Set current label
1856
+ const active = this.selectedFolder || (this.folders[0]?.name ?? '');
1857
+ if (active) {
1858
+ this.selectedFolder = active;
1859
+ label.textContent = active;
1860
+ }
1861
+ else {
1862
+ label.textContent = 'Select folder...';
1863
+ }
1864
+ menu.innerHTML = '';
1865
+ this.folders.forEach((f) => {
1866
+ const btn = document.createElement('button');
1867
+ btn.type = 'button';
1868
+ btn.className =
1869
+ 'ollang-folder-option' +
1870
+ (f.name === this.selectedFolder ? ' ollang-folder-option-active' : '');
1871
+ btn.textContent = f.name;
1872
+ btn.addEventListener('click', () => {
1873
+ this.selectedFolder = f.name;
1874
+ label.textContent = f.name;
1875
+ const items = menu.querySelectorAll('.ollang-folder-option');
1876
+ items.forEach((el) => el.classList.remove('ollang-folder-option-active'));
1877
+ btn.classList.add('ollang-folder-option-active');
1878
+ menu.setAttribute('data-open', 'false');
1879
+ menu.style.display = 'none';
1880
+ });
1881
+ menu.appendChild(btn);
1882
+ });
1883
+ }
1884
+ showNewFolderDialog() {
1885
+ if (document.getElementById('ollang-new-folder-container'))
1886
+ return;
1887
+ const parent = document.querySelector('#ollang-debug-panel #ollang-push-tms')?.parentElement;
1888
+ if (!parent)
1889
+ return;
1890
+ const container = document.createElement('div');
1891
+ container.id = 'ollang-new-folder-container';
1892
+ container.style.cssText =
1893
+ 'width: 100%; padding: 8px 15px 0 15px; display: flex; gap: 6px; align-items: center;';
1894
+ const input = document.createElement('input');
1895
+ input.type = 'text';
1896
+ input.placeholder = 'Enter folder name';
1897
+ input.style.cssText =
1898
+ 'flex: 1; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px;';
1899
+ const createBtn = document.createElement('button');
1900
+ createBtn.textContent = 'Create';
1901
+ createBtn.className = 'ollang-btn-sm';
1902
+ const cancelBtn = document.createElement('button');
1903
+ cancelBtn.textContent = 'Cancel';
1904
+ cancelBtn.className = 'ollang-btn-link';
1905
+ const remove = () => {
1906
+ if (container.parentElement)
1907
+ container.remove();
1908
+ };
1909
+ createBtn.addEventListener('click', () => {
1910
+ const name = input.value.trim();
1911
+ if (!name) {
1912
+ this.showStatus('Please enter a folder name', 'error');
1913
+ return;
1914
+ }
1915
+ this.selectedFolder = name;
1916
+ if (!this.folders.find((f) => f.name === name))
1917
+ this.folders.push({ id: name, name });
1918
+ this.updateFolderOptions();
1919
+ this.showStatus(`Folder "${name}" selected.`, 'success');
1920
+ remove();
1921
+ });
1922
+ cancelBtn.addEventListener('click', remove);
1923
+ container.appendChild(input);
1924
+ container.appendChild(createBtn);
1925
+ container.appendChild(cancelBtn);
1926
+ parent.parentElement?.insertBefore(container, parent.nextSibling);
1927
+ setTimeout(() => input.focus(), 0);
1928
+ }
1929
+ async fetchStrapiSchemaInPanel() {
1930
+ const baseUrl = (this.config.baseUrl || '').replace(/\/$/, '');
1931
+ if (!baseUrl) {
1932
+ this.showStatus('Missing TMS baseUrl', 'error');
1933
+ return;
1934
+ }
1935
+ const urlInput = document.getElementById('ollang-strapi-url');
1936
+ const jwtInput = document.getElementById('ollang-strapi-jwt');
1937
+ const btn = document.getElementById('ollang-fetch-schema');
1938
+ const strapiUrl = urlInput?.value?.trim();
1939
+ const strapiToken = jwtInput?.value?.trim();
1940
+ if (!strapiUrl || !strapiToken) {
1941
+ this.showStatus('Enter Strapi URL and Strapi API token', 'error');
1942
+ return;
1943
+ }
1944
+ if (btn)
1945
+ btn.disabled = true;
1946
+ this.showStatus('Fetching Strapi schema...', 'info');
1947
+ try {
1948
+ const res = await fetch(`${baseUrl}/api/strapi-schema`, {
1949
+ method: 'POST',
1950
+ headers: { 'Content-Type': 'application/json' },
1951
+ body: JSON.stringify({ strapiUrl, strapiToken }),
1952
+ });
1953
+ const data = await res.json().catch(() => ({}));
1954
+ if (res.ok && data.success) {
1955
+ const n = data.contentTypes?.length ?? 0;
1956
+ this.showStatus(`Schema loaded: ${n} content-type(s)`, 'success');
1957
+ }
1958
+ else {
1959
+ this.showStatus(data.error || `Failed (${res.status})`, 'error');
1960
+ }
1961
+ }
1962
+ catch (e) {
1963
+ this.showStatus('Network error: ' + (e instanceof Error ? e.message : String(e)), 'error');
1964
+ }
1965
+ finally {
1966
+ if (btn)
1967
+ btn.disabled = false;
1968
+ }
1969
+ }
1970
+ async pushToTMS() {
1971
+ if (this.selectedContentIds.size === 0) {
1972
+ this.showStatus('Please select at least one item', 'error');
1973
+ return;
1974
+ }
1975
+ if (!this.selectedFolder) {
1976
+ this.showStatus('Please select a folder first', 'error');
1977
+ return;
1978
+ }
1979
+ if (!this.config.apiKey) {
1980
+ this.showStatus('Please enter TMS API token first', 'error');
1981
+ return;
1982
+ }
1983
+ const baseUrl = (this.config.baseUrl || '').replace(/\/$/, '');
1984
+ if (!baseUrl) {
1985
+ this.showStatus('Missing baseUrl', 'error');
1986
+ return;
1987
+ }
1988
+ const selected = Array.from(this.capturedContent.values()).filter((c) => this.selectedContentIds.has(c.id));
1989
+ const mediaItems = selected.filter((c) => !!c.mediaUrl && (c.type === 'cms' || c.cmsType === 'strapi' || !!c.strapiContentType));
1990
+ const textItems = selected.filter((c) => !c.mediaUrl || !(c.type === 'cms' || c.cmsType === 'strapi' || !!c.strapiContentType));
1991
+ if (textItems.length === 0 && mediaItems.length === 0) {
1992
+ this.showStatus('Nothing to push. Please select at least one text or media item.', 'info');
1993
+ return;
1994
+ }
1995
+ this.showStatus(`Pushing ${textItems.length} text and ${mediaItems.length} media items to Ollang...`, 'info');
1996
+ try {
1997
+ const hasCmsItems = textItems.some((c) => c.type === 'cms' || c.cmsType === 'strapi' || !!c.strapiContentType) || mediaItems.length > 0;
1998
+ let strapiFieldConfig = {};
1999
+ const strapiBaseUrl = this.config.strapiUrl || [...this.detectedStrapiUrls][0] || '';
2000
+ if (hasCmsItems && strapiBaseUrl && baseUrl) {
2001
+ try {
2002
+ const configUrl = `${baseUrl}/api/strapi-field-config?strapiUrl=${encodeURIComponent(strapiBaseUrl.replace(/\/$/, ''))}`;
2003
+ const configRes = await fetch(configUrl, {
2004
+ headers: { 'Content-Type': 'application/json', 'x-api-key': this.config.apiKey },
2005
+ });
2006
+ if (configRes.ok) {
2007
+ const data = await configRes.json();
2008
+ if (data.fieldsByContentType && Object.keys(data.fieldsByContentType).length > 0) {
2009
+ strapiFieldConfig = data.fieldsByContentType;
2010
+ if (this.config.debug) {
2011
+ console.log('[Ollang] Using dynamic Strapi field config:', strapiFieldConfig);
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ catch (e) {
2017
+ if (this.config.debug)
2018
+ console.warn('[Ollang] Could not fetch Strapi field config:', e);
2019
+ }
2020
+ }
2021
+ const pathMatchesAllowed = (allowedPath, key) => {
2022
+ if (allowedPath === key)
2023
+ return true;
2024
+ if (allowedPath.includes('[]')) {
2025
+ const escaped = allowedPath.replace(/\./g, '\\.').replace(/\[\]/g, '\\.\\d+');
2026
+ const re = new RegExp(`^${escaped}$`);
2027
+ return re.test(key);
2028
+ }
2029
+ return false;
2030
+ };
2031
+ const getNestedValue = (obj, path) => {
2032
+ if (!obj || !path)
2033
+ return undefined;
2034
+ const parts = path.split('.');
2035
+ let cur = obj;
2036
+ for (const p of parts) {
2037
+ if (cur == null)
2038
+ return undefined;
2039
+ cur = cur[p];
2040
+ }
2041
+ return cur;
2042
+ };
2043
+ const serializeForCmsField = (value) => {
2044
+ if (value == null)
2045
+ return undefined;
2046
+ if (typeof value === 'string' || typeof value === 'number')
2047
+ return String(value);
2048
+ if (typeof value === 'object') {
2049
+ const url = value?.data?.attributes?.url ?? value?.url;
2050
+ if (typeof url === 'string')
2051
+ return url;
2052
+ try {
2053
+ return JSON.stringify(value);
2054
+ }
2055
+ catch {
2056
+ return undefined;
2057
+ }
2058
+ }
2059
+ return String(value);
2060
+ };
2061
+ const entryGroupMap = new Map();
2062
+ const nonCmsTextItems = [];
2063
+ for (const c of textItems) {
2064
+ if (c.strapiContentType && c.strapiEntryId != null) {
2065
+ const key = `${c.strapiContentType}:${c.strapiEntryId}`;
2066
+ if (!entryGroupMap.has(key)) {
2067
+ entryGroupMap.set(key, {
2068
+ items: [],
2069
+ entryData: this.strapiEntries.get(key),
2070
+ });
2071
+ }
2072
+ entryGroupMap.get(key).items.push(c);
2073
+ }
2074
+ else {
2075
+ nonCmsTextItems.push(c);
2076
+ }
2077
+ }
2078
+ for (const [key, group] of entryGroupMap) {
2079
+ const [contentType, entryIdStr] = key.split(':');
2080
+ const entryId = Number(entryIdStr);
2081
+ const hasDescription = group.items.some((i) => i.strapiField === 'description');
2082
+ if (!hasDescription) {
2083
+ const longKey = `${contentType}:${entryId}:description`;
2084
+ const longMeta = this.strapiLongTextMap.get(longKey);
2085
+ if (longMeta) {
2086
+ group.items.push({
2087
+ id: `api-desc-${contentType}-${entryId}`,
2088
+ text: longMeta.rawText,
2089
+ type: 'cms',
2090
+ selector: `api://${contentType}/${entryId}/description`,
2091
+ xpath: '',
2092
+ tagName: 'richtext',
2093
+ attributes: {},
2094
+ url: window.location.href,
2095
+ timestamp: Date.now(),
2096
+ cmsType: 'strapi',
2097
+ cmsField: 'description',
2098
+ cmsId: String(entryId),
2099
+ strapiContentType: contentType,
2100
+ strapiEntryId: entryId,
2101
+ strapiField: 'description',
2102
+ });
2103
+ }
2104
+ }
2105
+ }
2106
+ const entryTexts = Array.from(entryGroupMap.entries()).map(([key, group]) => {
2107
+ const [contentType, entryIdStr] = key.split(':');
2108
+ const entryId = Number(entryIdStr);
2109
+ const titleItem = group.items.find((i) => i.strapiField?.includes('title')) || group.items[0];
2110
+ const route = group.entryData?.attributes?.route ||
2111
+ group.items.find((i) => i.strapiRoute)?.strapiRoute ||
2112
+ null;
2113
+ const cmsFields = {};
2114
+ for (const item of group.items) {
2115
+ if (item.strapiField) {
2116
+ cmsFields[item.strapiField] = item.text;
2117
+ }
2118
+ }
2119
+ const allowedPaths = strapiFieldConfig[contentType] ??
2120
+ (contentType.endsWith('s') ? strapiFieldConfig[contentType.slice(0, -1)] : undefined);
2121
+ if (allowedPaths && allowedPaths.length > 0) {
2122
+ const filtered = {};
2123
+ for (const [path, value] of Object.entries(cmsFields)) {
2124
+ if (allowedPaths.some((p) => pathMatchesAllowed(p, path))) {
2125
+ filtered[path] = value;
2126
+ }
2127
+ }
2128
+ Object.keys(cmsFields).forEach((k) => delete cmsFields[k]);
2129
+ Object.assign(cmsFields, filtered);
2130
+ const attrs = group.entryData?.attributes;
2131
+ if (attrs) {
2132
+ for (const allowedPath of allowedPaths) {
2133
+ const normalizedPath = allowedPath.replace(/\[\]/g, '.0');
2134
+ const existingKey = Object.keys(cmsFields).find((k) => pathMatchesAllowed(allowedPath, k));
2135
+ if (existingKey)
2136
+ continue;
2137
+ const raw = getNestedValue(attrs, normalizedPath);
2138
+ const serialized = serializeForCmsField(raw);
2139
+ if (serialized !== undefined && serialized !== '') {
2140
+ cmsFields[allowedPath] = serialized;
2141
+ }
2142
+ }
2143
+ }
2144
+ }
2145
+ return {
2146
+ id: `cms-entry-${contentType}-${entryId}`,
2147
+ text: titleItem.text,
2148
+ type: 'cms',
2149
+ source: {
2150
+ file: titleItem.selector || 'browser-dom',
2151
+ line: 0,
2152
+ column: 0,
2153
+ context: titleItem.xpath || '',
2154
+ },
2155
+ strapiContentType: contentType,
2156
+ strapiEntryId: entryId,
2157
+ strapiField: titleItem.strapiField || 'header.title',
2158
+ strapiRoute: route,
2159
+ cmsFields,
2160
+ selected: false,
2161
+ status: 'scanned',
2162
+ };
2163
+ });
2164
+ const nonCmsTexts = nonCmsTextItems.map((c) => ({
2165
+ id: `cms-${c.id}`,
2166
+ text: c.text,
2167
+ type: (c.type === 'cms' ? 'cms' : 'dynamic'),
2168
+ source: {
2169
+ file: c.selector || 'browser-dom',
2170
+ line: 0,
2171
+ column: 0,
2172
+ context: c.xpath || '',
2173
+ },
2174
+ selected: false,
2175
+ status: 'scanned',
2176
+ }));
2177
+ const strapiEntries = Array.from(this.strapiEntries.values()).map((entry) => ({
2178
+ contentType: entry.contentType,
2179
+ entryId: entry.entryId,
2180
+ route: entry.attributes?.route || null,
2181
+ locale: entry.attributes?.locale || null,
2182
+ title: entry.attributes?.header?.title || entry.attributes?.title || null,
2183
+ }));
2184
+ const scanData = {
2185
+ texts: [...entryTexts, ...nonCmsTexts],
2186
+ media: mediaItems.map((c) => ({
2187
+ id: `media-${c.id}`,
2188
+ mediaUrl: c.mediaUrl,
2189
+ mediaType: c.mediaType,
2190
+ alt: c.mediaAlt,
2191
+ type: 'cms-media',
2192
+ source: {
2193
+ file: c.selector || 'browser-dom',
2194
+ line: 0,
2195
+ column: 0,
2196
+ context: c.xpath || '',
2197
+ },
2198
+ metadata: {
2199
+ selector: c.selector,
2200
+ xpath: c.xpath,
2201
+ tagName: c.tagName,
2202
+ attributes: c.attributes,
2203
+ cmsType: c.cmsType,
2204
+ cmsField: c.cmsField,
2205
+ cmsId: c.cmsId,
2206
+ strapiContentType: c.strapiContentType,
2207
+ strapiEntryId: c.strapiEntryId,
2208
+ strapiField: c.strapiField,
2209
+ strapiRoute: c.strapiRoute,
2210
+ },
2211
+ selected: false,
2212
+ status: 'scanned',
2213
+ })),
2214
+ isCms: hasCmsItems,
2215
+ cms: {
2216
+ strapi: {
2217
+ entries: strapiEntries,
2218
+ },
2219
+ },
2220
+ routes: this.getEntryRoutes(),
2221
+ timestamp: new Date().toISOString(),
2222
+ projectRoot: window.location.origin,
2223
+ sourceLanguage: 'en',
2224
+ targetLanguages: [],
2225
+ projectId: this.config.projectId || 'unknown',
2226
+ folderName: this.selectedFolder,
2227
+ };
2228
+ let existingScanId = null;
2229
+ try {
2230
+ const listRes = await fetch(`${baseUrl}/scans`, {
2231
+ headers: { 'Content-Type': 'application/json', 'x-api-key': this.config.apiKey },
2232
+ });
2233
+ if (listRes.ok) {
2234
+ const scans = await listRes.json();
2235
+ if (Array.isArray(scans)) {
2236
+ const url = window.location.href;
2237
+ let best = null;
2238
+ for (const scan of scans) {
2239
+ const sd = typeof scan.scanData === 'string' ? JSON.parse(scan.scanData) : scan.scanData;
2240
+ if (sd?.folderName !== this.selectedFolder)
2241
+ continue;
2242
+ const match = scan.url === url ||
2243
+ scan.originalUrl === url ||
2244
+ sd?.projectRoot === window.location.origin;
2245
+ if (!best || match) {
2246
+ best = scan;
2247
+ if (match)
2248
+ break;
2249
+ }
2250
+ }
2251
+ if (best)
2252
+ existingScanId = best.id || best._id;
2253
+ }
2254
+ }
2255
+ }
2256
+ catch (e) {
2257
+ console.warn('Failed to list scans:', e);
2258
+ }
2259
+ const payload = {
2260
+ url: window.location.href,
2261
+ scanData,
2262
+ originalFilename: `cms-scan-${Date.now()}.json`,
2263
+ folderName: this.selectedFolder,
2264
+ };
2265
+ const endpoint = existingScanId ? `${baseUrl}/scans/${existingScanId}` : `${baseUrl}/scans`;
2266
+ const method = existingScanId ? 'PATCH' : 'POST';
2267
+ const res = await fetch(endpoint, {
2268
+ method,
2269
+ headers: { 'Content-Type': 'application/json', 'x-api-key': this.config.apiKey },
2270
+ body: JSON.stringify(payload),
2271
+ });
2272
+ if (!res.ok) {
2273
+ const err = await res.json();
2274
+ throw new Error(err.message || `Failed: ${res.statusText}`);
2275
+ }
2276
+ const result = await res.json();
2277
+ this.showStatus(`✅ Pushed ${selected.length} items to Ollang! Scan ID: ${result.id || 'N/A'}`, 'success');
2278
+ this.selectedContentIds.clear();
2279
+ const list = document.getElementById('ollang-content-list');
2280
+ if (list)
2281
+ this.showContent(list);
2282
+ }
2283
+ catch (e) {
2284
+ console.error('Push to Ollang error:', e);
2285
+ this.showStatus(`❌ Failed to push to Ollang: ${e.message}`, 'error');
2286
+ }
2287
+ }
2288
+ }
2289
+ exports.OllangBrowser = OllangBrowser;
2290
+ OllangBrowser.MAX_FIELD_LENGTH = 500;
2291
+ OllangBrowser.LONG_TEXT_FIELDS = new Set([
2292
+ 'description',
2293
+ 'content',
2294
+ 'body',
2295
+ 'html',
2296
+ 'markdown',
2297
+ 'richText',
2298
+ 'text',
2299
+ ]);
2300
+ OllangBrowser.MAX_RECURSION_DEPTH = 4;
2301
+ OllangBrowser.SKIP_RELATION_KEYS = new Set([
2302
+ 'author',
2303
+ 'editor',
2304
+ 'localizations',
2305
+ 'category',
2306
+ 'categories',
2307
+ ]);
2308
+ OllangBrowser.SKIP_KEYS = new Set([
2309
+ 'id',
2310
+ 'createdAt',
2311
+ 'updatedAt',
2312
+ 'publishedAt',
2313
+ 'publishedDate',
2314
+ 'locale',
2315
+ 'route',
2316
+ 'url',
2317
+ 'path',
2318
+ 'slug',
2319
+ 'hash',
2320
+ 'ext',
2321
+ 'mime',
2322
+ 'provider',
2323
+ 'previewUrl',
2324
+ 'provider_metadata',
2325
+ 'background',
2326
+ 'name',
2327
+ 'alternativeText',
2328
+ 'caption',
2329
+ 'isInvisible',
2330
+ 'views',
2331
+ 'size',
2332
+ 'width',
2333
+ 'height',
2334
+ 'isStory',
2335
+ 'summary',
2336
+ ]);