@livenetworks/ashlar 2.2.0 → 2.2.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 (353) hide show
  1. package/assets/brand/android-chrome-192x192.png +0 -0
  2. package/assets/brand/android-chrome-512x512.png +0 -0
  3. package/assets/brand/apple-touch-icon.png +0 -0
  4. package/assets/brand/favicon-16x16.png +0 -0
  5. package/assets/brand/favicon-32x32.png +0 -0
  6. package/assets/brand/favicon-48x48.png +0 -0
  7. package/assets/brand/favicon-dark-128x128.png +0 -0
  8. package/assets/brand/favicon-dark-16x16.png +0 -0
  9. package/assets/brand/favicon-dark-180x180.png +0 -0
  10. package/assets/brand/favicon-dark-192x192.png +0 -0
  11. package/assets/brand/favicon-dark-256x256.png +0 -0
  12. package/assets/brand/favicon-dark-32x32.png +0 -0
  13. package/assets/brand/favicon-dark-48x48.png +0 -0
  14. package/assets/brand/favicon-dark-512x512.png +0 -0
  15. package/assets/brand/favicon-dark-64x64.png +0 -0
  16. package/assets/brand/favicon-dark.ico +0 -0
  17. package/assets/brand/favicon-dark.png +0 -0
  18. package/assets/brand/favicon-light-128x128.png +0 -0
  19. package/assets/brand/favicon-light-16x16.png +0 -0
  20. package/assets/brand/favicon-light-180x180.png +0 -0
  21. package/assets/brand/favicon-light-192x192.png +0 -0
  22. package/assets/brand/favicon-light-256x256.png +0 -0
  23. package/assets/brand/favicon-light-32x32.png +0 -0
  24. package/assets/brand/favicon-light-48x48.png +0 -0
  25. package/assets/brand/favicon-light-512x512.png +0 -0
  26. package/assets/brand/favicon-light-64x64.png +0 -0
  27. package/assets/brand/favicon-light.ico +0 -0
  28. package/assets/brand/favicon-light.png +0 -0
  29. package/assets/brand/favicon.ico +0 -0
  30. package/assets/brand/favicon.png +0 -0
  31. package/assets/brand/favicon.svg +12 -0
  32. package/assets/brand/ln-ashlar-emblem-dark-128.png +0 -0
  33. package/assets/brand/ln-ashlar-emblem-dark-16.png +0 -0
  34. package/assets/brand/ln-ashlar-emblem-dark-256.png +0 -0
  35. package/assets/brand/ln-ashlar-emblem-dark-32.png +0 -0
  36. package/assets/brand/ln-ashlar-emblem-dark-64.png +0 -0
  37. package/assets/brand/ln-ashlar-emblem-dark.png +0 -0
  38. package/assets/brand/ln-ashlar-emblem-light-128.png +0 -0
  39. package/assets/brand/ln-ashlar-emblem-light-16.png +0 -0
  40. package/assets/brand/ln-ashlar-emblem-light-256.png +0 -0
  41. package/assets/brand/ln-ashlar-emblem-light-32.png +0 -0
  42. package/assets/brand/ln-ashlar-emblem-light-64.png +0 -0
  43. package/assets/brand/ln-ashlar-emblem-light.png +0 -0
  44. package/assets/brand/ln-ashlar-logo-dark-128.png +0 -0
  45. package/assets/brand/ln-ashlar-logo-dark-256.png +0 -0
  46. package/assets/brand/ln-ashlar-logo-dark-512.png +0 -0
  47. package/assets/brand/ln-ashlar-logo-dark.png +0 -0
  48. package/assets/brand/ln-ashlar-logo-light-128.png +0 -0
  49. package/assets/brand/ln-ashlar-logo-light-256.png +0 -0
  50. package/assets/brand/ln-ashlar-logo-light-512.png +0 -0
  51. package/assets/brand/ln-ashlar-logo-light.png +0 -0
  52. package/assets/brand/site.webmanifest +41 -0
  53. package/assets/brand/source/ln-ashlar-icon-dark-1024.png +0 -0
  54. package/assets/brand/source/ln-ashlar-icon-dark-uncropped-1024.png +0 -0
  55. package/assets/brand/source/ln-ashlar-icon-light-1024.png +0 -0
  56. package/assets/brand/source/ln-ashlar-icon-light-uncropped-1024.png +0 -0
  57. package/assets/flags/ad.svg +150 -0
  58. package/assets/flags/ae.svg +6 -0
  59. package/assets/flags/af.svg +81 -0
  60. package/assets/flags/ag.svg +14 -0
  61. package/assets/flags/ai.svg +29 -0
  62. package/assets/flags/al.svg +5 -0
  63. package/assets/flags/am.svg +5 -0
  64. package/assets/flags/ao.svg +13 -0
  65. package/assets/flags/aq.svg +5 -0
  66. package/assets/flags/ar.svg +32 -0
  67. package/assets/flags/arab.svg +109 -0
  68. package/assets/flags/as.svg +72 -0
  69. package/assets/flags/asean.svg +13 -0
  70. package/assets/flags/at.svg +4 -0
  71. package/assets/flags/au.svg +8 -0
  72. package/assets/flags/aw.svg +186 -0
  73. package/assets/flags/ax.svg +18 -0
  74. package/assets/flags/az.svg +8 -0
  75. package/assets/flags/ba.svg +12 -0
  76. package/assets/flags/bb.svg +6 -0
  77. package/assets/flags/bd.svg +4 -0
  78. package/assets/flags/be.svg +7 -0
  79. package/assets/flags/bf.svg +7 -0
  80. package/assets/flags/bg.svg +5 -0
  81. package/assets/flags/bh.svg +4 -0
  82. package/assets/flags/bi.svg +15 -0
  83. package/assets/flags/bj.svg +14 -0
  84. package/assets/flags/bl.svg +5 -0
  85. package/assets/flags/bm.svg +97 -0
  86. package/assets/flags/bn.svg +36 -0
  87. package/assets/flags/bo.svg +673 -0
  88. package/assets/flags/bq.svg +5 -0
  89. package/assets/flags/br.svg +45 -0
  90. package/assets/flags/bs.svg +13 -0
  91. package/assets/flags/bt.svg +89 -0
  92. package/assets/flags/bv.svg +13 -0
  93. package/assets/flags/bw.svg +7 -0
  94. package/assets/flags/by.svg +18 -0
  95. package/assets/flags/bz.svg +145 -0
  96. package/assets/flags/ca.svg +4 -0
  97. package/assets/flags/cc.svg +19 -0
  98. package/assets/flags/cd.svg +5 -0
  99. package/assets/flags/cefta.svg +13 -0
  100. package/assets/flags/cf.svg +15 -0
  101. package/assets/flags/cg.svg +12 -0
  102. package/assets/flags/ch.svg +9 -0
  103. package/assets/flags/ci.svg +7 -0
  104. package/assets/flags/ck.svg +9 -0
  105. package/assets/flags/cl.svg +13 -0
  106. package/assets/flags/cm.svg +15 -0
  107. package/assets/flags/cn.svg +11 -0
  108. package/assets/flags/co.svg +7 -0
  109. package/assets/flags/cp.svg +7 -0
  110. package/assets/flags/cr.svg +7 -0
  111. package/assets/flags/cu.svg +13 -0
  112. package/assets/flags/cv.svg +13 -0
  113. package/assets/flags/cw.svg +14 -0
  114. package/assets/flags/cx.svg +15 -0
  115. package/assets/flags/cy.svg +6 -0
  116. package/assets/flags/cz.svg +5 -0
  117. package/assets/flags/de.svg +5 -0
  118. package/assets/flags/dg.svg +130 -0
  119. package/assets/flags/dj.svg +13 -0
  120. package/assets/flags/dk.svg +5 -0
  121. package/assets/flags/dm.svg +152 -0
  122. package/assets/flags/do.svg +121 -0
  123. package/assets/flags/dz.svg +5 -0
  124. package/assets/flags/eac.svg +48 -0
  125. package/assets/flags/ec.svg +138 -0
  126. package/assets/flags/ee.svg +5 -0
  127. package/assets/flags/eg.svg +38 -0
  128. package/assets/flags/eh.svg +16 -0
  129. package/assets/flags/er.svg +8 -0
  130. package/assets/flags/es-ct.svg +4 -0
  131. package/assets/flags/es-ga.svg +187 -0
  132. package/assets/flags/es-pv.svg +5 -0
  133. package/assets/flags/es.svg +544 -0
  134. package/assets/flags/et.svg +14 -0
  135. package/assets/flags/eu.svg +28 -0
  136. package/assets/flags/fi.svg +5 -0
  137. package/assets/flags/fj.svg +120 -0
  138. package/assets/flags/fk.svg +90 -0
  139. package/assets/flags/fm.svg +11 -0
  140. package/assets/flags/fo.svg +12 -0
  141. package/assets/flags/fr.svg +5 -0
  142. package/assets/flags/ga.svg +7 -0
  143. package/assets/flags/gb-eng.svg +5 -0
  144. package/assets/flags/gb-nir.svg +132 -0
  145. package/assets/flags/gb-sct.svg +4 -0
  146. package/assets/flags/gb-wls.svg +9 -0
  147. package/assets/flags/gb.svg +7 -0
  148. package/assets/flags/gd.svg +27 -0
  149. package/assets/flags/ge.svg +6 -0
  150. package/assets/flags/gf.svg +5 -0
  151. package/assets/flags/gg.svg +9 -0
  152. package/assets/flags/gh.svg +6 -0
  153. package/assets/flags/gi.svg +32 -0
  154. package/assets/flags/gl.svg +4 -0
  155. package/assets/flags/gm.svg +14 -0
  156. package/assets/flags/gn.svg +7 -0
  157. package/assets/flags/gp.svg +5 -0
  158. package/assets/flags/gq.svg +23 -0
  159. package/assets/flags/gr.svg +16 -0
  160. package/assets/flags/gs.svg +133 -0
  161. package/assets/flags/gt.svg +204 -0
  162. package/assets/flags/gu.svg +19 -0
  163. package/assets/flags/gw.svg +13 -0
  164. package/assets/flags/gy.svg +9 -0
  165. package/assets/flags/hk.svg +8 -0
  166. package/assets/flags/hm.svg +8 -0
  167. package/assets/flags/hn.svg +18 -0
  168. package/assets/flags/hr.svg +58 -0
  169. package/assets/flags/ht.svg +116 -0
  170. package/assets/flags/hu.svg +7 -0
  171. package/assets/flags/ic.svg +7 -0
  172. package/assets/flags/id.svg +4 -0
  173. package/assets/flags/ie.svg +7 -0
  174. package/assets/flags/il.svg +14 -0
  175. package/assets/flags/im.svg +36 -0
  176. package/assets/flags/in.svg +25 -0
  177. package/assets/flags/io.svg +130 -0
  178. package/assets/flags/iq.svg +10 -0
  179. package/assets/flags/ir.svg +219 -0
  180. package/assets/flags/is.svg +12 -0
  181. package/assets/flags/it.svg +7 -0
  182. package/assets/flags/je.svg +62 -0
  183. package/assets/flags/jm.svg +8 -0
  184. package/assets/flags/jo.svg +16 -0
  185. package/assets/flags/jp.svg +11 -0
  186. package/assets/flags/ke.svg +23 -0
  187. package/assets/flags/kg.svg +4 -0
  188. package/assets/flags/kh.svg +61 -0
  189. package/assets/flags/ki.svg +36 -0
  190. package/assets/flags/km.svg +16 -0
  191. package/assets/flags/kn.svg +14 -0
  192. package/assets/flags/kp.svg +15 -0
  193. package/assets/flags/kr.svg +24 -0
  194. package/assets/flags/kw.svg +13 -0
  195. package/assets/flags/ky.svg +103 -0
  196. package/assets/flags/kz.svg +36 -0
  197. package/assets/flags/la.svg +12 -0
  198. package/assets/flags/lb.svg +15 -0
  199. package/assets/flags/lc.svg +8 -0
  200. package/assets/flags/li.svg +43 -0
  201. package/assets/flags/lk.svg +22 -0
  202. package/assets/flags/lr.svg +14 -0
  203. package/assets/flags/ls.svg +8 -0
  204. package/assets/flags/lt.svg +7 -0
  205. package/assets/flags/lu.svg +5 -0
  206. package/assets/flags/lv.svg +6 -0
  207. package/assets/flags/ly.svg +13 -0
  208. package/assets/flags/ma.svg +4 -0
  209. package/assets/flags/mc.svg +6 -0
  210. package/assets/flags/md.svg +70 -0
  211. package/assets/flags/me.svg +116 -0
  212. package/assets/flags/mf.svg +5 -0
  213. package/assets/flags/mg.svg +7 -0
  214. package/assets/flags/mh.svg +7 -0
  215. package/assets/flags/mk.svg +5 -0
  216. package/assets/flags/ml.svg +7 -0
  217. package/assets/flags/mm.svg +12 -0
  218. package/assets/flags/mn.svg +14 -0
  219. package/assets/flags/mo.svg +9 -0
  220. package/assets/flags/mp.svg +86 -0
  221. package/assets/flags/mq.svg +5 -0
  222. package/assets/flags/mr.svg +6 -0
  223. package/assets/flags/ms.svg +29 -0
  224. package/assets/flags/mt.svg +58 -0
  225. package/assets/flags/mu.svg +8 -0
  226. package/assets/flags/mv.svg +6 -0
  227. package/assets/flags/mw.svg +10 -0
  228. package/assets/flags/mx.svg +382 -0
  229. package/assets/flags/my.svg +26 -0
  230. package/assets/flags/mz.svg +21 -0
  231. package/assets/flags/na.svg +16 -0
  232. package/assets/flags/nc.svg +13 -0
  233. package/assets/flags/ne.svg +6 -0
  234. package/assets/flags/nf.svg +9 -0
  235. package/assets/flags/ng.svg +6 -0
  236. package/assets/flags/ni.svg +129 -0
  237. package/assets/flags/nl.svg +5 -0
  238. package/assets/flags/no.svg +7 -0
  239. package/assets/flags/np.svg +13 -0
  240. package/assets/flags/nr.svg +12 -0
  241. package/assets/flags/nu.svg +10 -0
  242. package/assets/flags/nz.svg +36 -0
  243. package/assets/flags/om.svg +115 -0
  244. package/assets/flags/pa.svg +14 -0
  245. package/assets/flags/pc.svg +33 -0
  246. package/assets/flags/pe.svg +4 -0
  247. package/assets/flags/pf.svg +19 -0
  248. package/assets/flags/pg.svg +9 -0
  249. package/assets/flags/ph.svg +6 -0
  250. package/assets/flags/pk.svg +15 -0
  251. package/assets/flags/pl.svg +6 -0
  252. package/assets/flags/pm.svg +5 -0
  253. package/assets/flags/pn.svg +53 -0
  254. package/assets/flags/pr.svg +13 -0
  255. package/assets/flags/ps.svg +6 -0
  256. package/assets/flags/pt.svg +57 -0
  257. package/assets/flags/pw.svg +11 -0
  258. package/assets/flags/py.svg +157 -0
  259. package/assets/flags/qa.svg +4 -0
  260. package/assets/flags/re.svg +5 -0
  261. package/assets/flags/ro.svg +7 -0
  262. package/assets/flags/rs.svg +292 -0
  263. package/assets/flags/ru.svg +5 -0
  264. package/assets/flags/rw.svg +13 -0
  265. package/assets/flags/sa.svg +25 -0
  266. package/assets/flags/sb.svg +13 -0
  267. package/assets/flags/sc.svg +7 -0
  268. package/assets/flags/sd.svg +13 -0
  269. package/assets/flags/se.svg +4 -0
  270. package/assets/flags/sg.svg +13 -0
  271. package/assets/flags/sh-ac.svg +689 -0
  272. package/assets/flags/sh-hl.svg +164 -0
  273. package/assets/flags/sh-ta.svg +76 -0
  274. package/assets/flags/sh.svg +7 -0
  275. package/assets/flags/si.svg +18 -0
  276. package/assets/flags/sj.svg +7 -0
  277. package/assets/flags/sk.svg +9 -0
  278. package/assets/flags/sl.svg +7 -0
  279. package/assets/flags/sm.svg +75 -0
  280. package/assets/flags/sn.svg +8 -0
  281. package/assets/flags/so.svg +11 -0
  282. package/assets/flags/sr.svg +6 -0
  283. package/assets/flags/ss.svg +8 -0
  284. package/assets/flags/st.svg +16 -0
  285. package/assets/flags/sv.svg +593 -0
  286. package/assets/flags/sx.svg +56 -0
  287. package/assets/flags/sy.svg +6 -0
  288. package/assets/flags/sz.svg +34 -0
  289. package/assets/flags/tc.svg +50 -0
  290. package/assets/flags/td.svg +7 -0
  291. package/assets/flags/tf.svg +15 -0
  292. package/assets/flags/tg.svg +14 -0
  293. package/assets/flags/th.svg +7 -0
  294. package/assets/flags/tj.svg +22 -0
  295. package/assets/flags/tk.svg +5 -0
  296. package/assets/flags/tl.svg +13 -0
  297. package/assets/flags/tm.svg +204 -0
  298. package/assets/flags/tn.svg +4 -0
  299. package/assets/flags/to.svg +10 -0
  300. package/assets/flags/tr.svg +8 -0
  301. package/assets/flags/tt.svg +5 -0
  302. package/assets/flags/tv.svg +9 -0
  303. package/assets/flags/tw.svg +34 -0
  304. package/assets/flags/tz.svg +13 -0
  305. package/assets/flags/ua.svg +6 -0
  306. package/assets/flags/ug.svg +30 -0
  307. package/assets/flags/um.svg +9 -0
  308. package/assets/flags/un.svg +16 -0
  309. package/assets/flags/us.svg +9 -0
  310. package/assets/flags/uy.svg +28 -0
  311. package/assets/flags/uz.svg +30 -0
  312. package/assets/flags/va.svg +190 -0
  313. package/assets/flags/vc.svg +8 -0
  314. package/assets/flags/ve.svg +26 -0
  315. package/assets/flags/vg.svg +59 -0
  316. package/assets/flags/vi.svg +28 -0
  317. package/assets/flags/vn.svg +11 -0
  318. package/assets/flags/vu.svg +21 -0
  319. package/assets/flags/wf.svg +5 -0
  320. package/assets/flags/ws.svg +7 -0
  321. package/assets/flags/xk.svg +5 -0
  322. package/assets/flags/xx.svg +4 -0
  323. package/assets/flags/ye.svg +7 -0
  324. package/assets/flags/yt.svg +5 -0
  325. package/assets/flags/za.svg +17 -0
  326. package/assets/flags/zm.svg +27 -0
  327. package/assets/flags/zw.svg +21 -0
  328. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  329. package/components/ln-date/ln-date.js +1 -1
  330. package/components/ln-debug/ln-debug.js +1 -1
  331. package/components/ln-debug/src/generated-attributes.js +306 -304
  332. package/components/ln-external-links/ln-external-links.js +1 -1
  333. package/components/ln-filter/ln-filter.js +1 -1
  334. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  335. package/components/ln-sort/ln-sort.js +1 -1
  336. package/components/ln-table/ln-table.js +1 -1
  337. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  338. package/components/ln-toast/ln-toast.js +1 -1
  339. package/components/ln-translations/README.md +202 -120
  340. package/components/ln-translations/ln-translations.js +1 -1
  341. package/components/ln-translations/ln-translations.schema.json +136 -120
  342. package/components/ln-translations/src/ln-translations.js +354 -295
  343. package/components/ln-translations/src/translations-model.js +221 -0
  344. package/components/ln-validate/ln-validate.js +1 -1
  345. package/ln-ashlar.html-data.json +1832 -1824
  346. package/package.json +96 -95
  347. package/theme/components/_translations.scss +118 -111
  348. package/theme/config/mixins/_app-shell.scss +14 -0
  349. package/theme/config/mixins/_count.scss +48 -0
  350. package/theme/config/mixins/_index.scss +1 -0
  351. package/theme/config/mixins/_nav.scss +3 -11
  352. package/theme/config/mixins/_translations.scss +126 -124
  353. package/web-types.json +2365 -2351
@@ -1,295 +1,354 @@
1
- import { cloneTemplate, dispatch, dispatchCancelable, registerComponent, defineAttrs, attrSpec, attrStr } from '../../ln-core';
2
-
3
- (function () {
4
- const DOM_SELECTOR = 'data-ln-translations';
5
- const DOM_ATTRIBUTE = 'lnTranslations';
6
-
7
- if (window[DOM_ATTRIBUTE] !== undefined) return;
8
-
9
- // ─── Attribute Contract (SSOT) ──────────────────────────
10
- const ATTRIBUTES = {
11
- 'data-ln-translations': { type: 'marker', description: 'Mounts multi-language translation manager on form container' },
12
- 'data-ln-translations-default': { prop: 'defaultLang', read: attrStr, type: 'string', fallback: '', description: 'Default primary language code (e.g. en)' },
13
- 'data-ln-translations-placeholder': { prop: 'placeholderLabel', read: attrStr, type: 'string', fallback: '{lang} translation', description: 'Placeholder label pattern for cloned translation inputs' },
14
- 'data-ln-translations-remove-label': { prop: 'removeLabel', read: attrStr, type: 'string', fallback: 'Remove {lang}', description: 'Accessible label template for translation removal buttons' },
15
- 'data-ln-translations-locales': { prop: '_localesRaw', read: attrStr, type: 'json', fallback: '', description: 'JSON dictionary of supported locale codes and display labels' },
16
- 'data-ln-translations-active': { type: 'marker', description: 'Container holding active language badge tags' },
17
- 'data-ln-translations-add': { type: 'string', description: 'Trigger button action to add specified language translation fields' },
18
- 'data-ln-translations-lang': { type: 'string', description: 'Language code associated with active language badge' },
19
- 'data-ln-translations-prefix': { type: 'string', description: 'Prefix format pattern for cloned translated field names' },
20
- 'data-ln-translatable': { type: 'marker', description: 'Marks form control as translatable into multiple languages' },
21
- 'data-ln-translatable-lang': { type: 'string', description: 'Language code assigned to specific translatable input instance' }
22
- };
23
- const ATTR_SPEC = attrSpec(ATTRIBUTES);
24
-
25
- // ─── Default locales (override via data-ln-translations-locales JSON) ──
26
-
27
- const DEFAULT_LOCALES = {
28
- en: 'English',
29
- sq: 'Shqip',
30
- sr: 'Srpski'
31
- };
32
-
33
- // ─── Component ─────────────────────────────────────────────
34
-
35
- function _component(dom) {
36
- this.dom = dom;
37
- defineAttrs(this, dom, ATTR_SPEC);
38
- this.activeLanguages = new Set();
39
- this.badgesEl = dom.querySelector('[data-ln-translations-active]');
40
- this.menuEl = dom.querySelector('[data-ln-dropdown] > [data-ln-toggle]');
41
-
42
- // Parse locales from attribute or use defaults
43
- this.locales = DEFAULT_LOCALES;
44
- if (this._localesRaw) {
45
- try { this.locales = JSON.parse(this._localesRaw); }
46
- catch (e) { console.warn('[ln-translations] Invalid JSON in data-ln-translations-locales'); }
47
- }
48
-
49
- // Set default language flag on original inputs
50
- this._applyDefaultLang();
51
-
52
- // Populate dropdown menu
53
- this._updateDropdown();
54
-
55
- // Bind request events
56
- const self = this;
57
- this._onRequestAdd = function (e) {
58
- if (e.detail && e.detail.lang) self.addLanguage(e.detail.lang);
59
- };
60
- this._onRequestRemove = function (e) {
61
- if (e.detail && e.detail.lang) self.removeLanguage(e.detail.lang);
62
- };
63
- dom.addEventListener('ln-translations:request-add', this._onRequestAdd);
64
- dom.addEventListener('ln-translations:request-remove', this._onRequestRemove);
65
-
66
- // Detect existing translations in DOM (server-rendered)
67
- this._detectExisting();
68
-
69
- return this;
70
- }
71
-
72
- // ─── Default language flag ─────────────────────────────────
73
-
74
- _component.prototype._applyDefaultLang = function () {
75
- if (!this.defaultLang) return;
76
-
77
- const translatables = this.dom.querySelectorAll('[data-ln-translatable]');
78
- for (const wrapper of translatables) {
79
- const originals = wrapper.querySelectorAll('input:not([data-ln-translatable-lang]), textarea:not([data-ln-translatable-lang]), select:not([data-ln-translatable-lang])');
80
- for (const el of originals) {
81
- el.setAttribute('data-ln-translatable-lang', this.defaultLang);
82
- }
83
- }
84
- };
85
-
86
- // ─── Detect existing translations ──────────────────────────
87
-
88
- _component.prototype._detectExisting = function () {
89
- const existing = this.dom.querySelectorAll('[data-ln-translatable-lang]');
90
- for (const el of existing) {
91
- const lang = el.getAttribute('data-ln-translatable-lang');
92
- if (lang && lang !== this.defaultLang) {
93
- this.activeLanguages.add(lang);
94
- }
95
- }
96
-
97
- if (this.activeLanguages.size > 0) {
98
- this._updateBadges();
99
- this._updateDropdown();
100
- }
101
- };
102
-
103
- // ─── Dropdown update ───────────────────────────────────────
104
-
105
- _component.prototype._updateDropdown = function () {
106
- if (!this.menuEl) return;
107
-
108
- this.menuEl.textContent = '';
109
- const self = this;
110
- let availableCount = 0;
111
-
112
- for (const lang in this.locales) {
113
- if (!this.locales.hasOwnProperty(lang)) continue;
114
- if (this.activeLanguages.has(lang)) continue;
115
- availableCount++;
116
-
117
- const frag = cloneTemplate('ln-translations-menu-item', 'ln-translations');
118
- if (!frag) return;
119
- const btn = frag.querySelector('[data-ln-translations-lang]');
120
- btn.setAttribute('data-ln-translations-lang', lang);
121
- btn.textContent = this.locales[lang];
122
-
123
- btn.addEventListener('click', function (e) {
124
- if (e.ctrlKey || e.metaKey || e.button === 1) return;
125
- e.preventDefault();
126
- e.stopPropagation();
127
- if (self.menuEl.getAttribute('data-ln-toggle') === 'open') {
128
- self.menuEl.setAttribute('data-ln-toggle', 'close');
129
- }
130
- self.addLanguage(lang);
131
- });
132
-
133
- this.menuEl.appendChild(frag);
134
- }
135
-
136
- // Hide trigger if no languages available
137
- const triggerBtn = this.dom.querySelector('[data-ln-translations-add]');
138
- if (triggerBtn) {
139
- triggerBtn.hidden = availableCount === 0;
140
- }
141
- };
142
-
143
- // ─── Badges update ─────────────────────────────────────────
144
-
145
- _component.prototype._updateBadges = function () {
146
- if (!this.badgesEl) return;
147
-
148
- this.badgesEl.textContent = '';
149
- const self = this;
150
-
151
- this.activeLanguages.forEach(function (lang) {
152
- const frag = cloneTemplate('ln-translations-badge', 'ln-translations');
153
- if (!frag) return;
154
- const p = frag.querySelector('[data-ln-translations-lang]');
155
- p.setAttribute('data-ln-translations-lang', lang);
156
-
157
- const label = p.querySelector('span');
158
- label.textContent = self.locales[lang] || lang.toUpperCase();
159
-
160
- const closeBtn = p.querySelector('button');
161
- const removeLangName = self.locales[lang] || lang.toUpperCase();
162
- closeBtn.setAttribute('aria-label', self.removeLabel.replace('{lang}', removeLangName));
163
-
164
- closeBtn.addEventListener('click', function (e) {
165
- if (e.ctrlKey || e.metaKey || e.button === 1) return;
166
- e.preventDefault();
167
- e.stopPropagation();
168
- self.removeLanguage(lang);
169
- });
170
-
171
- self.badgesEl.appendChild(frag);
172
- });
173
- };
174
-
175
- // ─── Public API ────────────────────────────────────────────
176
-
177
- _component.prototype.addLanguage = function (lang, values) {
178
- if (this.activeLanguages.has(lang)) return;
179
-
180
- const langName = this.locales[lang] || lang;
181
- const before = dispatchCancelable(this.dom, 'ln-translations:before-add', {
182
- target: this.dom, lang: lang, langName: langName
183
- });
184
- if (before.defaultPrevented) return;
185
-
186
- this.activeLanguages.add(lang);
187
- values = values || {};
188
-
189
- // Clone inputs for each translatable field
190
- const translatables = this.dom.querySelectorAll('[data-ln-translatable]');
191
- for (const wrapper of translatables) {
192
- const field = wrapper.getAttribute('data-ln-translatable');
193
- const prefix = wrapper.getAttribute('data-ln-translations-prefix') || '';
194
-
195
- // Find the original input/textarea (first one without data-ln-translatable-lang, or with default lang)
196
- const original = wrapper.querySelector(
197
- this.defaultLang
198
- ? '[data-ln-translatable-lang="' + this.defaultLang + '"]'
199
- : 'input:not([data-ln-translatable-lang]), textarea:not([data-ln-translatable-lang]), select:not([data-ln-translatable-lang])'
200
- );
201
- if (!original) continue;
202
-
203
- const clone = original.cloneNode(original.tagName === 'SELECT');
204
-
205
- // Set name
206
- if (prefix) {
207
- clone.name = prefix + '[trans][' + lang + '][' + field + ']';
208
- } else {
209
- clone.name = 'trans[' + lang + '][' + field + ']';
210
- }
211
-
212
- // Set value
213
- clone.value = (values[field] !== undefined) ? values[field] : '';
214
-
215
- // Remove id to avoid duplicates
216
- clone.removeAttribute('id');
217
-
218
- // Set placeholder (selects have no placeholder property)
219
- if ('placeholder' in clone) {
220
- clone.placeholder = this.placeholderLabel.replace('{lang}', langName);
221
- }
222
-
223
- // Mark with language attribute
224
- clone.setAttribute('data-ln-translatable-lang', lang);
225
-
226
- // Insert after original or after last clone for this field
227
- const existing = wrapper.querySelectorAll('[data-ln-translatable-lang]:not([data-ln-translatable-lang="' + this.defaultLang + '"])');
228
- const insertAfter = existing.length > 0 ? existing[existing.length - 1] : original;
229
- insertAfter.parentNode.insertBefore(clone, insertAfter.nextSibling);
230
- }
231
-
232
- this._updateDropdown();
233
- this._updateBadges();
234
-
235
- dispatch(this.dom, 'ln-translations:added', {
236
- target: this.dom, lang: lang, langName: langName
237
- });
238
- };
239
-
240
- _component.prototype.removeLanguage = function (lang) {
241
- if (!this.activeLanguages.has(lang)) return;
242
-
243
- const before = dispatchCancelable(this.dom, 'ln-translations:before-remove', {
244
- target: this.dom, lang: lang
245
- });
246
- if (before.defaultPrevented) return;
247
-
248
- // Remove all clones for this language
249
- const clones = this.dom.querySelectorAll('[data-ln-translatable-lang="' + lang + '"]');
250
- for (const clone of clones) {
251
- clone.parentNode.removeChild(clone);
252
- }
253
-
254
- this.activeLanguages.delete(lang);
255
- this._updateDropdown();
256
- this._updateBadges();
257
-
258
- dispatch(this.dom, 'ln-translations:removed', {
259
- target: this.dom, lang: lang
260
- });
261
- };
262
-
263
- _component.prototype.getActiveLanguages = function () {
264
- return new Set(this.activeLanguages);
265
- };
266
-
267
- _component.prototype.hasLanguage = function (lang) {
268
- return this.activeLanguages.has(lang);
269
- };
270
-
271
- _component.prototype.destroy = function () {
272
- if (!this.dom[DOM_ATTRIBUTE]) return;
273
-
274
- // Remove all translation clones (not default lang)
275
- const defaultLang = this.defaultLang;
276
- const clones = this.dom.querySelectorAll('[data-ln-translatable-lang]');
277
- for (const clone of clones) {
278
- if (clone.getAttribute('data-ln-translatable-lang') !== defaultLang) {
279
- clone.parentNode.removeChild(clone);
280
- }
281
- }
282
-
283
- // Remove event listeners
284
- this.dom.removeEventListener('ln-translations:request-add', this._onRequestAdd);
285
- this.dom.removeEventListener('ln-translations:request-remove', this._onRequestRemove);
286
-
287
- delete this.dom[DOM_ATTRIBUTE];
288
- };
289
-
290
- // ─── Init ──────────────────────────────────────────────────
291
-
292
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-translations', {
293
- attributes: ATTRIBUTES
294
- });
295
- })();
1
+ import {
2
+ buildDict,
3
+ cloneTemplateScoped,
4
+ cloneTemplate,
5
+ dispatch,
6
+ dispatchCancelable,
7
+ getLocale,
8
+ registerComponent,
9
+ defineAttrs,
10
+ attrSpec,
11
+ attrStr,
12
+ attrJson,
13
+ shouldIgnoreClick
14
+ } from '../../ln-core';
15
+
16
+ import {
17
+ buildFieldName,
18
+ formatPlaceholder,
19
+ formatRemoveLabel,
20
+ parseLocales,
21
+ resolveLanguageName,
22
+ filterAvailableLocales
23
+ } from './translations-model.js';
24
+
25
+ (function () {
26
+ const DOM_SELECTOR = 'data-ln-translations';
27
+ const DOM_ATTRIBUTE = 'lnTranslations';
28
+
29
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
30
+
31
+ // ─── Attribute Contract (SSOT) ──────────────────────────
32
+ const ATTRIBUTES = {
33
+ 'data-ln-translations': { type: 'marker', description: 'Mounts multi-language translation coordinator on form container' },
34
+ 'data-ln-translations-default': { prop: 'defaultLang', read: attrStr, type: 'string', fallback: '', description: 'Default primary language code (e.g. en)' },
35
+ 'data-ln-translations-placeholder': { prop: 'placeholderLabel', read: attrStr, type: 'string', fallback: '{lang} translation', description: 'Placeholder label pattern for cloned translation inputs' },
36
+ 'data-ln-translations-remove-label': { prop: 'removeLabel', read: attrStr, type: 'string', fallback: 'Remove {lang}', description: 'Accessible label template for translation removal buttons' },
37
+ 'data-ln-translations-locales': { prop: '_localesRaw', read: attrJson, type: 'json', fallback: null, description: 'JSON dictionary of supported locale codes and display labels' },
38
+ 'data-ln-translations-dict': { type: 'string', description: 'Dictionary key prefix mapping for translatable labels' },
39
+ 'data-ln-translations-menu': { type: 'marker', description: 'Menu container holding available language items' },
40
+ 'data-ln-translations-active': { type: 'marker', description: 'Container holding active language badge tags' },
41
+ 'data-ln-translations-add': { type: 'trigger', description: 'Trigger button to open translation language menu' },
42
+ 'data-ln-translations-lang': { type: 'string', description: 'Language code associated with active language badge or menu item' },
43
+ 'data-ln-translations-prefix': { type: 'string', description: 'Prefix format pattern for nested translatable field names' },
44
+ 'data-ln-translatable': { type: 'string', description: 'Marks form control container as translatable, value is field name' },
45
+ 'data-ln-translatable-lang': { type: 'string', description: 'Language code assigned to specific translatable input instance' }
46
+ };
47
+ const ATTR_SPEC = attrSpec(ATTRIBUTES);
48
+
49
+ // ─── Component Constructor ─────────────────────────────
50
+
51
+ function _component(dom) {
52
+ this.dom = dom;
53
+ defineAttrs(this, dom, ATTR_SPEC);
54
+ this.dict = buildDict(dom, 'data-ln-translations-dict');
55
+ this.locale = getLocale(dom);
56
+ this.activeLanguages = new Set();
57
+
58
+ this.badgesEl = dom.querySelector('[data-ln-translations-active]');
59
+ this.menuEl = dom.querySelector('[data-ln-translations-menu]') || dom.querySelector('[data-ln-dropdown] > [data-ln-toggle]');
60
+
61
+ // Parse locales from attribute (or empty fallback)
62
+ this.locales = parseLocales(this._localesRaw, {});
63
+
64
+ // Flag original inputs with default language
65
+ this._applyDefaultLang();
66
+
67
+ const self = this;
68
+
69
+ // Delegated click listener for dropdown menu items
70
+ if (this.menuEl) {
71
+ this._boundMenuClick = function (e) {
72
+ if (shouldIgnoreClick(e)) return;
73
+ const btn = e.target.closest('button[data-ln-translations-lang]');
74
+ if (!btn || !self.menuEl.contains(btn)) return;
75
+
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+
79
+ const lang = btn.getAttribute('data-ln-translations-lang');
80
+ if (self.menuEl.getAttribute('data-ln-toggle') === 'open') {
81
+ self.menuEl.setAttribute('data-ln-toggle', 'close');
82
+ }
83
+ if (lang) {
84
+ self.addLanguage(lang);
85
+ }
86
+ };
87
+ this.menuEl.addEventListener('click', this._boundMenuClick);
88
+ }
89
+
90
+ // Delegated click listener for active badges remove button
91
+ if (this.badgesEl) {
92
+ this._boundBadgesClick = function (e) {
93
+ if (shouldIgnoreClick(e)) return;
94
+ const btn = e.target.closest('button');
95
+ if (!btn || !self.badgesEl.contains(btn)) return;
96
+ const item = btn.closest('[data-ln-translations-lang]');
97
+ const lang = item ? item.getAttribute('data-ln-translations-lang') : null;
98
+ if (lang) {
99
+ e.preventDefault();
100
+ e.stopPropagation();
101
+ self.removeLanguage(lang);
102
+ }
103
+ };
104
+ this.badgesEl.addEventListener('click', this._boundBadgesClick);
105
+ }
106
+
107
+ // Populate dropdown menu
108
+ this._updateDropdown();
109
+
110
+ // Bind request events
111
+ this._onRequestAdd = function (e) {
112
+ if (e.detail && e.detail.lang) {
113
+ self.addLanguage(e.detail.lang, e.detail.values);
114
+ }
115
+ };
116
+ this._onRequestRemove = function (e) {
117
+ if (e.detail && e.detail.lang) {
118
+ self.removeLanguage(e.detail.lang);
119
+ }
120
+ };
121
+ dom.addEventListener('ln-translations:request-add', this._onRequestAdd);
122
+ dom.addEventListener('ln-translations:request-remove', this._onRequestRemove);
123
+
124
+ // Detect pre-rendered server translations
125
+ this._detectExisting();
126
+
127
+ return this;
128
+ }
129
+
130
+ // ─── Default language flag ─────────────────────────────────
131
+
132
+ _component.prototype._applyDefaultLang = function () {
133
+ if (!this.defaultLang) return;
134
+
135
+ const translatables = this.dom.querySelectorAll('[data-ln-translatable]');
136
+ for (const wrapper of translatables) {
137
+ const originals = wrapper.querySelectorAll('input:not([data-ln-translatable-lang]), textarea:not([data-ln-translatable-lang]), select:not([data-ln-translatable-lang])');
138
+ for (const el of originals) {
139
+ el.setAttribute('data-ln-translatable-lang', this.defaultLang);
140
+ }
141
+ }
142
+ };
143
+
144
+ // ─── Detect existing translations ──────────────────────────
145
+
146
+ _component.prototype._detectExisting = function () {
147
+ const existing = this.dom.querySelectorAll('[data-ln-translatable-lang]');
148
+ for (const el of existing) {
149
+ const lang = el.getAttribute('data-ln-translatable-lang');
150
+ if (lang && lang !== this.defaultLang) {
151
+ this.activeLanguages.add(lang);
152
+ }
153
+ }
154
+
155
+ if (this.activeLanguages.size > 0) {
156
+ this._updateBadges();
157
+ this._updateDropdown();
158
+ }
159
+ };
160
+
161
+ // ─── Dropdown update ───────────────────────────────────────
162
+
163
+ _component.prototype._updateDropdown = function () {
164
+ if (!this.menuEl) return;
165
+
166
+ this.menuEl.textContent = '';
167
+ const available = filterAvailableLocales(this.locales, this.activeLanguages, this.defaultLang);
168
+
169
+ for (let i = 0; i < available.length; i++) {
170
+ const [lang, langName] = available[i];
171
+ const frag = cloneTemplateScoped(this.dom, 'ln-translations-menu-item') || cloneTemplate('ln-translations-menu-item', 'ln-translations');
172
+ if (!frag) return;
173
+
174
+ const btn = frag.querySelector('[data-ln-translations-lang]');
175
+ if (!btn) continue;
176
+ btn.setAttribute('data-ln-translations-lang', lang);
177
+ btn.textContent = langName;
178
+
179
+ this.menuEl.appendChild(frag);
180
+ }
181
+
182
+ // Hide trigger if no languages available
183
+ const triggerBtn = this.dom.querySelector('[data-ln-translations-add]');
184
+ if (triggerBtn) {
185
+ triggerBtn.hidden = available.length === 0;
186
+ }
187
+ };
188
+
189
+ // ─── Badges update ─────────────────────────────────────────
190
+
191
+ _component.prototype._updateBadges = function () {
192
+ if (!this.badgesEl) return;
193
+
194
+ this.badgesEl.textContent = '';
195
+ const self = this;
196
+ const removePattern = (this.dict && this.dict['remove-label']) || this.removeLabel;
197
+
198
+ this.activeLanguages.forEach(function (lang) {
199
+ const frag = cloneTemplateScoped(self.dom, 'ln-translations-badge') || cloneTemplate('ln-translations-badge', 'ln-translations');
200
+ if (!frag) return;
201
+
202
+ const p = frag.querySelector('[data-ln-translations-lang]');
203
+ if (!p) return;
204
+ p.setAttribute('data-ln-translations-lang', lang);
205
+
206
+ const langName = resolveLanguageName(lang, self.locales, self.locale);
207
+ const label = p.querySelector('span');
208
+ if (label) {
209
+ label.textContent = langName;
210
+ }
211
+
212
+ const closeBtn = p.querySelector('button');
213
+ if (closeBtn) {
214
+ closeBtn.setAttribute('aria-label', formatRemoveLabel(removePattern, langName));
215
+ }
216
+
217
+ self.badgesEl.appendChild(frag);
218
+ });
219
+ };
220
+
221
+ // ─── Public API ────────────────────────────────────────────
222
+
223
+ _component.prototype.addLanguage = function (lang, values) {
224
+ if (!lang || lang === this.defaultLang || this.activeLanguages.has(lang)) return;
225
+
226
+ const langName = resolveLanguageName(lang, this.locales, this.locale);
227
+ const before = dispatchCancelable(this.dom, 'ln-translations:before-add', {
228
+ target: this.dom, lang: lang, langName: langName
229
+ });
230
+ if (before.defaultPrevented) return;
231
+
232
+ this.activeLanguages.add(lang);
233
+ values = values || {};
234
+
235
+ const placeholderPattern = (this.dict && this.dict['placeholder']) || this.placeholderLabel;
236
+ const placeholderText = formatPlaceholder(placeholderPattern, langName);
237
+
238
+ // Clone inputs for each translatable field
239
+ const translatables = this.dom.querySelectorAll('[data-ln-translatable]');
240
+ for (const wrapper of translatables) {
241
+ const field = wrapper.getAttribute('data-ln-translatable');
242
+ const prefix = wrapper.getAttribute('data-ln-translations-prefix') || '';
243
+
244
+ // The original input is the base field (flagged defaultLang or first input)
245
+ const original = wrapper.querySelector('[data-ln-translatable-lang="' + this.defaultLang + '"]') || wrapper.querySelector('input, textarea, select');
246
+ if (!original) continue;
247
+
248
+ const clone = original.cloneNode(original.tagName === 'SELECT');
249
+
250
+ // Set deterministic nested name: trans[lang][field]
251
+ clone.name = buildFieldName(field, lang, prefix);
252
+
253
+ // Set value
254
+ clone.value = (values[field] !== undefined) ? values[field] : '';
255
+
256
+ // Remove id to avoid duplicates in document
257
+ clone.removeAttribute('id');
258
+
259
+ // Set placeholder and accessible label
260
+ if ('placeholder' in clone) {
261
+ clone.placeholder = placeholderText;
262
+ }
263
+ clone.setAttribute('aria-label', placeholderText);
264
+ clone.setAttribute('data-ln-translatable-lang', lang);
265
+
266
+ // Insert after the last translation or after original
267
+ const existing = wrapper.querySelectorAll('[data-ln-translatable-lang]');
268
+ const insertAfter = existing.length > 0 ? existing[existing.length - 1] : original;
269
+ insertAfter.parentNode.insertBefore(clone, insertAfter.nextSibling);
270
+ }
271
+
272
+ this._updateDropdown();
273
+ this._updateBadges();
274
+
275
+ dispatch(this.dom, 'ln-translations:added', {
276
+ target: this.dom, lang: lang, langName: langName
277
+ });
278
+ };
279
+
280
+ _component.prototype.removeLanguage = function (lang) {
281
+ // Default language can never be removed; only active translations
282
+ if (!lang || lang === this.defaultLang || !this.activeLanguages.has(lang)) return;
283
+
284
+ const before = dispatchCancelable(this.dom, 'ln-translations:before-remove', {
285
+ target: this.dom, lang: lang
286
+ });
287
+ if (before.defaultPrevented) return;
288
+
289
+ // Remove all translation inputs for this language
290
+ const fields = this.dom.querySelectorAll('[data-ln-translatable-lang="' + lang + '"]');
291
+ for (const el of fields) {
292
+ el.parentNode.removeChild(el);
293
+ }
294
+
295
+ this.activeLanguages.delete(lang);
296
+ this._updateDropdown();
297
+ this._updateBadges();
298
+
299
+ dispatch(this.dom, 'ln-translations:removed', {
300
+ target: this.dom, lang: lang
301
+ });
302
+ };
303
+
304
+ _component.prototype.getActiveLanguages = function () {
305
+ return new Set(this.activeLanguages);
306
+ };
307
+
308
+ _component.prototype.hasLanguage = function (lang) {
309
+ return this.activeLanguages.has(lang);
310
+ };
311
+
312
+ _component.prototype.destroy = function () {
313
+ if (!this.dom[DOM_ATTRIBUTE]) return;
314
+
315
+ // Remove all dynamically added translation inputs
316
+ for (const lang of this.activeLanguages) {
317
+ const fields = this.dom.querySelectorAll('[data-ln-translatable-lang="' + lang + '"]');
318
+ for (const el of fields) {
319
+ el.parentNode.removeChild(el);
320
+ }
321
+ }
322
+
323
+ if (this.badgesEl) {
324
+ this.badgesEl.textContent = '';
325
+ if (this._boundBadgesClick) {
326
+ this.badgesEl.removeEventListener('click', this._boundBadgesClick);
327
+ this._boundBadgesClick = null;
328
+ }
329
+ }
330
+
331
+ if (this.menuEl) {
332
+ this.menuEl.textContent = '';
333
+ if (this._boundMenuClick) {
334
+ this.menuEl.removeEventListener('click', this._boundMenuClick);
335
+ this._boundMenuClick = null;
336
+ }
337
+ }
338
+
339
+ const triggerBtn = this.dom.querySelector('[data-ln-translations-add]');
340
+ if (triggerBtn) triggerBtn.hidden = false;
341
+
342
+ this.dom.removeEventListener('ln-translations:request-add', this._onRequestAdd);
343
+ this.dom.removeEventListener('ln-translations:request-remove', this._onRequestRemove);
344
+
345
+ this.activeLanguages.clear();
346
+ delete this.dom[DOM_ATTRIBUTE];
347
+ };
348
+
349
+ // ─── Init ──────────────────────────────────────────────────
350
+
351
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-translations', {
352
+ attributes: ATTRIBUTES
353
+ });
354
+ })();