@jungherz-de/notionkit-elements 1.16.0 → 1.17.0
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.
- package/CHANGELOG.md +17 -0
- package/README.md +64 -6
- package/SKILL.md +59 -53
- package/dist/components/base.js +102 -3
- package/dist/components/nk-ai-input-row.js +6 -3
- package/dist/components/nk-board-view.js +2 -2
- package/dist/components/nk-breadcrumb.js +2 -1
- package/dist/components/nk-calendar-view.js +8 -5
- package/dist/components/nk-calendar.js +10 -7
- package/dist/components/nk-cmdk.js +6 -2
- package/dist/components/nk-comments.js +5 -3
- package/dist/components/nk-copy-field.js +6 -3
- package/dist/components/nk-emoji-picker.js +6 -2
- package/dist/components/nk-filter-bar.js +10 -8
- package/dist/components/nk-gallery-view.js +29 -7
- package/dist/components/nk-image-picker.js +7 -3
- package/dist/components/nk-list-view.js +3 -2
- package/dist/components/nk-modal.js +3 -1
- package/dist/components/nk-page-title.js +2 -1
- package/dist/components/nk-page.js +3 -1
- package/dist/components/nk-peek.js +6 -3
- package/dist/components/nk-sidebar.js +5 -2
- package/dist/components/nk-steps.js +20 -4
- package/dist/components/nk-synced.js +2 -1
- package/dist/components/nk-table-view.js +3 -2
- package/dist/components/nk-theme-toggle.js +5 -2
- package/dist/components/nk-tree-item.js +3 -1
- package/dist/components/shared/{property-cell-BSVcETss.js → property-cell-DcsfnUNF.js} +15 -5
- package/dist/notionkit-elements.esm.js +265 -70
- package/dist/notionkit-elements.esm.min.js +1 -1
- package/dist/notionkit-elements.esm.min.js.map +1 -1
- package/dist/notionkit-elements.js +267 -69
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +32 -2
- package/src/components/data/nk-ai-input-row.js +6 -3
- package/src/components/data/nk-board-view.js +2 -2
- package/src/components/data/nk-calendar-view.js +7 -4
- package/src/components/data/nk-comments.js +5 -3
- package/src/components/data/nk-filter-bar.js +10 -8
- package/src/components/data/nk-gallery-view.js +29 -7
- package/src/components/data/nk-list-view.js +2 -1
- package/src/components/data/nk-table-view.js +2 -1
- package/src/components/forms/nk-calendar.js +10 -7
- package/src/components/forms/nk-copy-field.js +6 -3
- package/src/components/forms/nk-image-picker.js +7 -3
- package/src/components/overlays/nk-cmdk.js +6 -2
- package/src/components/overlays/nk-emoji-picker.js +6 -2
- package/src/components/overlays/nk-modal.js +3 -1
- package/src/components/overlays/nk-peek.js +6 -3
- package/src/components/page/nk-page-title.js +2 -1
- package/src/components/page/nk-page.js +3 -1
- package/src/components/page/nk-steps.js +20 -4
- package/src/components/page/nk-synced.js +2 -1
- package/src/components/shell/nk-breadcrumb.js +2 -1
- package/src/components/shell/nk-sidebar.js +5 -2
- package/src/components/shell/nk-theme-toggle.js +5 -2
- package/src/components/shell/nk-tree-item.js +3 -1
- package/src/index.js +5 -1
- package/src/util/property-cell.js +14 -4
- package/src/util/strings.js +73 -0
|
@@ -17,6 +17,12 @@ import { NkElement } from '../../base.js';
|
|
|
17
17
|
// nk-select { index, value, step } – index counts from 1, like `current` –
|
|
18
18
|
// and, unless cancelled, makes that step current. `horizontal` sets the
|
|
19
19
|
// steps in one row above a wizard.
|
|
20
|
+
// A step with `href` (1.17.0) shows its label as a link, <a class="st-label">,
|
|
21
|
+
// with or without `selectable` – to open a chapter in a new tab or copy its
|
|
22
|
+
// address. A plain click, or Enter, fires the same nk-select; cancelling it
|
|
23
|
+
// keeps the browser from following the link, for a router of your own. A
|
|
24
|
+
// middle click or one with Cmd, Ctrl, Shift or Alt fires nothing and does
|
|
25
|
+
// what a link does.
|
|
20
26
|
class NkSteps extends NkElement {
|
|
21
27
|
static get observedAttributes() { return ['steps', 'current', 'label', 'selectable', 'horizontal']; }
|
|
22
28
|
|
|
@@ -44,7 +50,7 @@ class NkSteps extends NkElement {
|
|
|
44
50
|
// Rebuilding drops the focused label; the new one at that place takes it back.
|
|
45
51
|
const focused = this.shadowRoot?.activeElement?.closest?.('li')?.dataset.index;
|
|
46
52
|
this._list.replaceChildren(...this._items().map((_, i) => {
|
|
47
|
-
const n = i + 1, { label: text, desc, state } = this._step(i);
|
|
53
|
+
const n = i + 1, { label: text, desc, state, href } = this._step(i);
|
|
48
54
|
// A state of its own wins; without one, the steps before `current` are done.
|
|
49
55
|
const done = state ? state === 'done' : n < current, skipped = state === 'skipped';
|
|
50
56
|
const li = this.createElement('li', ['nk-step'], { 'data-index': String(n) });
|
|
@@ -54,7 +60,8 @@ class NkSteps extends NkElement {
|
|
|
54
60
|
if (n === current) li.setAttribute('aria-current', 'step');
|
|
55
61
|
const mark = this.createElement('span', ['st-mark'], { 'aria-hidden': 'true' });
|
|
56
62
|
mark.textContent = done ? '✓' : skipped ? '–' : String(n);
|
|
57
|
-
const body =
|
|
63
|
+
const body = href != null && href !== '' ? this.createElement('a', ['st-label'], { href: String(href) })
|
|
64
|
+
: selectable ? this.createElement('button', ['st-label'], { type: 'button' }) : document.createElement('span');
|
|
58
65
|
body.textContent = text ?? '';
|
|
59
66
|
if (desc) { const d = this.createElement('span', ['st-desc']); d.textContent = desc; body.appendChild(d); }
|
|
60
67
|
li.append(mark, body);
|
|
@@ -65,9 +72,18 @@ class NkSteps extends NkElement {
|
|
|
65
72
|
|
|
66
73
|
setupEvents() {
|
|
67
74
|
this._onClick = (e) => {
|
|
68
|
-
if (!this.getBoolAttr('selectable')) return;
|
|
69
75
|
const li = e.target.closest?.('li.nk-step');
|
|
70
|
-
if (li
|
|
76
|
+
if (!li || !this._list.contains(li)) return;
|
|
77
|
+
const link = li.querySelector('a.st-label');
|
|
78
|
+
if (link) {
|
|
79
|
+
// The link contract: a plain left click is a selection, any other
|
|
80
|
+
// one the browser's. The mark beside the link counts as the link.
|
|
81
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
82
|
+
if (!link.contains(e.target)) { link.click(); return; }
|
|
83
|
+
if (!this.select(Number(li.dataset.index))) e.preventDefault();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (this.getBoolAttr('selectable')) this.select(Number(li.dataset.index));
|
|
71
87
|
};
|
|
72
88
|
this._list.addEventListener('click', this._onClick);
|
|
73
89
|
}
|
|
@@ -14,8 +14,9 @@ class NkSynced extends NkElement {
|
|
|
14
14
|
this._sync();
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
_sync() { this._badge.textContent = this.getAttribute('badge') ?? '
|
|
17
|
+
_sync() { this._badge.textContent = this.getAttribute('badge') ?? this.str('synced'); }
|
|
18
18
|
onAttributeChanged() { this._sync(); }
|
|
19
|
+
onStringsChanged() { this._sync(); }
|
|
19
20
|
|
|
20
21
|
get badge() { return this.getAttribute('badge'); }
|
|
21
22
|
set badge(v) { this.setAttribute('badge', v); }
|
|
@@ -16,7 +16,7 @@ class NkBreadcrumb extends NkElement {
|
|
|
16
16
|
static get observesLightDom() { return true; }
|
|
17
17
|
|
|
18
18
|
render() {
|
|
19
|
-
this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': '
|
|
19
|
+
this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': this.str('breadcrumb') });
|
|
20
20
|
this._wrapper.appendChild(this._nav);
|
|
21
21
|
this.projectLightDom();
|
|
22
22
|
}
|
|
@@ -68,6 +68,7 @@ class NkBreadcrumb extends NkElement {
|
|
|
68
68
|
teardownEvents() { this._nav?.removeEventListener('click', this._onClick); }
|
|
69
69
|
|
|
70
70
|
onAttributeChanged() { this.projectLightDom(); }
|
|
71
|
+
onStringsChanged() { this._nav.setAttribute('aria-label', this.str('breadcrumb')); }
|
|
71
72
|
|
|
72
73
|
get separator() { return this.getAttribute('separator') ?? '/'; }
|
|
73
74
|
set separator(v) { this.setAttribute('separator', v); }
|
|
@@ -23,7 +23,8 @@ import { deepActiveElement } from '../../util/focus.js';
|
|
|
23
23
|
// brings it back; ⌘\ or Ctrl+\ toggles it (on a phone the drawer).
|
|
24
24
|
// `collapsed` is the state, collapse() / expand() / toggleCollapsed() the
|
|
25
25
|
// API, nk-collapse { collapsed } the moment to keep it. collapse-label names
|
|
26
|
-
// the « (default "Close sidebar"
|
|
26
|
+
// the « (default "Close sidebar", in German "Seitenleiste schließen") and is
|
|
27
|
+
// its tooltip for <nk-tooltip>.
|
|
27
28
|
const COLLAPSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m18 17-5-5 5-5M11 17l-5-5 5-5"/></svg>';
|
|
28
29
|
const phone = () => matchMedia('(max-width: 860px)').matches;
|
|
29
30
|
|
|
@@ -53,7 +54,7 @@ class NkSidebar extends NkElement {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
_syncCollapse() {
|
|
56
|
-
const label = this.getAttribute('collapse-label') || '
|
|
57
|
+
const label = this.getAttribute('collapse-label') || this.str('closeSidebar');
|
|
57
58
|
this._collapse.hidden = !this.getBoolAttr('collapsible');
|
|
58
59
|
this._collapse.setAttribute('aria-label', label);
|
|
59
60
|
this._collapse.dataset.tooltip = label;
|
|
@@ -105,6 +106,8 @@ class NkSidebar extends NkElement {
|
|
|
105
106
|
document.removeEventListener('keydown', this._onKey);
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
onStringsChanged() { this._syncCollapse(); }
|
|
110
|
+
|
|
108
111
|
onAttributeChanged(name) {
|
|
109
112
|
if (name === 'open') {
|
|
110
113
|
this._syncOpen();
|
|
@@ -14,7 +14,7 @@ class NkThemeToggle extends NkElement {
|
|
|
14
14
|
|
|
15
15
|
render() {
|
|
16
16
|
this._btn = this.createElement('button', ['nk-topbar-btn', 'nk-theme-toggle'], { type: 'button' });
|
|
17
|
-
this.
|
|
17
|
+
this._syncTitle();
|
|
18
18
|
this._wrapper.appendChild(this._btn);
|
|
19
19
|
const root = document.documentElement;
|
|
20
20
|
let stored = null;
|
|
@@ -45,8 +45,11 @@ class NkThemeToggle extends NkElement {
|
|
|
45
45
|
window.removeEventListener('message', this._onMessage);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
_syncTitle() { this._btn.title = this.getAttribute('title') || this.str('toggleTheme'); }
|
|
49
|
+
onStringsChanged() { this._syncTitle(); }
|
|
50
|
+
|
|
48
51
|
onAttributeChanged(name) {
|
|
49
|
-
if (name === 'title' && this._btn) this.
|
|
52
|
+
if (name === 'title' && this._btn) this._syncTitle();
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/** Sets the theme on <html>, persists it and reports it. */
|
|
@@ -33,7 +33,7 @@ class NkTreeItem extends NkElement {
|
|
|
33
33
|
this._labelSlot.appendChild(this._labelText);
|
|
34
34
|
this._label.appendChild(this._labelSlot);
|
|
35
35
|
this._actions = this.createElement('span', ['actions']);
|
|
36
|
-
for (const [action, glyph, title] of [['add', '+', '
|
|
36
|
+
for (const [action, glyph, title] of [['add', '+', this.str('add')], ['more', '⋯', this.str('more')]]) {
|
|
37
37
|
const b = this.createElement('span', [], { 'data-action': action, role: 'button', title });
|
|
38
38
|
b.textContent = glyph;
|
|
39
39
|
this._actions.appendChild(b);
|
|
@@ -119,6 +119,8 @@ class NkTreeItem extends NkElement {
|
|
|
119
119
|
this._row?.removeEventListener('keydown', this._onKey);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
onStringsChanged() { for (const b of this._actions.children) b.title = this.str(b.dataset.action); }
|
|
123
|
+
|
|
122
124
|
onAttributeChanged(name) {
|
|
123
125
|
if (name === 'label') this.assignSlots();
|
|
124
126
|
else this._sync();
|
package/src/index.js
CHANGED
|
@@ -117,4 +117,8 @@ export { NkComment } from './components/data/nk-comment.js';
|
|
|
117
117
|
export { NkAiThread } from './components/data/nk-ai-thread.js';
|
|
118
118
|
export { NkAiMsg } from './components/data/nk-ai-msg.js';
|
|
119
119
|
export { NkAiInputRow } from './components/data/nk-ai-input-row.js';
|
|
120
|
-
export { renderPropertyCell, tagFor, compareBy, formatDate, timeOf, textOf } from './util/property-cell.js';
|
|
120
|
+
export { renderPropertyCell, tagFor, compareBy, formatDate, formatPercent, timeOf, textOf } from './util/property-cell.js';
|
|
121
|
+
|
|
122
|
+
// 1.17.0 – the built-in texts in English and German, after the lang of the
|
|
123
|
+
// page or of the element; setStrings() adds or replaces them.
|
|
124
|
+
export { setStrings, builtInStrings } from './util/strings.js';
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// text: a string, or { text, desc, color, tooltip, sort } – one of Notion's
|
|
14
14
|
// nine text colours for a tone, a quiet second line under it, the whole
|
|
15
15
|
// text in a tooltip (served by <nk-tooltip>), a value to sort by.
|
|
16
|
+
// progress: 0–100, the bar and its percentage as Intl writes it in
|
|
17
|
+
// column.locale, else the page's language ("45 %" in German).
|
|
16
18
|
// date: the value as given, or with column.format ('short', 'relative' or
|
|
17
19
|
// Intl.DateTimeFormat options) and column.locale formatted from an ISO date
|
|
18
20
|
// or date-time, D.M.YYYY, a Date or a timestamp; { start, end } is a range.
|
|
@@ -80,7 +82,7 @@ export function renderPropertyCell(column, value, row = {}) {
|
|
|
80
82
|
if (type === 'actions') return actions(column, value);
|
|
81
83
|
if (value === undefined || value === null || value === '') {
|
|
82
84
|
if (type === 'checkbox') return checkbox(column, false, row);
|
|
83
|
-
if (type === 'progress') return progress(0);
|
|
85
|
+
if (type === 'progress') return progress(column, 0);
|
|
84
86
|
return el('span', type === 'person' ? 'person-cell' : null, type === 'text' && column.title ? '' : '—');
|
|
85
87
|
}
|
|
86
88
|
switch (type) {
|
|
@@ -112,7 +114,7 @@ export function renderPropertyCell(column, value, row = {}) {
|
|
|
112
114
|
}
|
|
113
115
|
// column.locale / column.format: Intl.NumberFormat locale and options, e.g. { minimumFractionDigits: 1 }.
|
|
114
116
|
case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString(column.locale, column.format) : String(value));
|
|
115
|
-
case 'progress': return progress(Number(value) || 0);
|
|
117
|
+
case 'progress': return progress(column, Number(value) || 0);
|
|
116
118
|
default: {
|
|
117
119
|
const o = typeof value === 'object' ? value : { text: value };
|
|
118
120
|
let t;
|
|
@@ -158,15 +160,23 @@ function checkbox(column, checked, row) {
|
|
|
158
160
|
return label;
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
function progress(value) {
|
|
163
|
+
function progress(column, value) {
|
|
162
164
|
const frag = document.createDocumentFragment();
|
|
163
165
|
const bar = el('span', 'nk-progress');
|
|
164
166
|
const fill = el('i'); fill.style.width = `${Math.max(0, Math.min(100, value))}%`;
|
|
165
167
|
bar.appendChild(fill);
|
|
166
|
-
frag.append(bar, el('span', 'nk-progress-label',
|
|
168
|
+
frag.append(bar, el('span', 'nk-progress-label', formatPercent(column, value)));
|
|
167
169
|
return frag;
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
/** A progress value of 0–100 as Intl writes a percentage in column.locale, else the page's language: 45 → "45%", in German "45 %". */
|
|
173
|
+
export function formatPercent(column, value) {
|
|
174
|
+
const n = Number(value) || 0;
|
|
175
|
+
try {
|
|
176
|
+
return new Intl.NumberFormat(column.locale || resolveLocale(), { style: 'percent', maximumFractionDigits: 0 }).format(n / 100);
|
|
177
|
+
} catch { return `${Math.round(n)}%`; } // a lang Intl does not take
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
/** A select's place among its options: the order Notion sorts by. */
|
|
171
181
|
function rankOf(column, value) {
|
|
172
182
|
const i = (column.options || []).findIndex(o => o.value === value || o.label === value);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The texts the elements show and announce by themselves – button labels,
|
|
2
|
+
// placeholders, accessible names – in English and German (NotionKit
|
|
3
|
+
// Elements 1.17.0). The language is the element's own: the nearest `lang`
|
|
4
|
+
// attribute, across shadow roots, else <html lang>; German for "de…",
|
|
5
|
+
// English for anything else. setStrings() adds or replaces texts, for one
|
|
6
|
+
// language or for all of them, and an attribute on the element wins over
|
|
7
|
+
// every one of these.
|
|
8
|
+
const BUILT_IN = {
|
|
9
|
+
en: {
|
|
10
|
+
copy: 'Copy', copied: 'Copied', show: 'Show', hide: 'Hide',
|
|
11
|
+
uploadImage: 'Upload image', changeImage: 'Change image', remove: 'Remove',
|
|
12
|
+
close: 'Close', resize: 'Resize',
|
|
13
|
+
today: 'Today', previousMonth: 'Previous month', nextMonth: 'Next month', clear: 'Clear', time: 'Time',
|
|
14
|
+
filter: 'Filter', sort: 'Sort', addFilter: '+ Filter', removeFilter: 'Remove filter', search: 'Search …',
|
|
15
|
+
closeSidebar: 'Close sidebar', newPage: '+ New page',
|
|
16
|
+
commentPlaceholder: 'Comment …', send: 'Send', askPlaceholder: 'Ask something …',
|
|
17
|
+
commandPlaceholder: 'Search or type a command …', emojiPlaceholder: 'Search…',
|
|
18
|
+
toggleTheme: 'Toggle light / dark', synced: '⟳ synced',
|
|
19
|
+
message: 'Message', breadcrumb: 'Breadcrumb', commandPalette: 'Command palette', comment: 'Comment',
|
|
20
|
+
searchEmoji: 'Search emoji', settings: 'Settings', title: 'Title', changeIcon: 'Change icon',
|
|
21
|
+
add: 'Add', more: 'More',
|
|
22
|
+
},
|
|
23
|
+
de: {
|
|
24
|
+
copy: 'Kopieren', copied: 'Kopiert', show: 'Zeigen', hide: 'Verbergen',
|
|
25
|
+
uploadImage: 'Bild hochladen', changeImage: 'Bild ändern', remove: 'Entfernen',
|
|
26
|
+
close: 'Schließen', resize: 'Breite ändern',
|
|
27
|
+
today: 'Heute', previousMonth: 'Voriger Monat', nextMonth: 'Nächster Monat', clear: 'Leeren', time: 'Uhrzeit',
|
|
28
|
+
filter: 'Filter', sort: 'Sortieren', addFilter: '+ Filter', removeFilter: 'Filter entfernen', search: 'Suchen …',
|
|
29
|
+
closeSidebar: 'Seitenleiste schließen', newPage: '+ Neue Seite',
|
|
30
|
+
commentPlaceholder: 'Kommentieren …', send: 'Senden', askPlaceholder: 'Frag etwas …',
|
|
31
|
+
commandPlaceholder: 'Suchen oder Befehl eingeben …', emojiPlaceholder: 'Suchen …',
|
|
32
|
+
toggleTheme: 'Hell / Dunkel', synced: '⟳ synchronisiert',
|
|
33
|
+
message: 'Nachricht', breadcrumb: 'Pfad', commandPalette: 'Befehlspalette', comment: 'Kommentar',
|
|
34
|
+
searchEmoji: 'Emoji suchen', settings: 'Einstellungen', title: 'Titel', changeIcon: 'Symbol ändern',
|
|
35
|
+
add: 'Hinzufügen', more: 'Mehr',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const custom = {};
|
|
39
|
+
const listeners = new Set();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Adds or replaces texts: setStrings({ copy: 'Copy link' }) for every
|
|
43
|
+
* language, setStrings({ copy: 'Copier' }, 'fr') for one – a language of its
|
|
44
|
+
* own too. The elements on the page take them at once.
|
|
45
|
+
*/
|
|
46
|
+
export function setStrings(strings, lang = '*') {
|
|
47
|
+
const key = String(lang).toLowerCase().split('-')[0];
|
|
48
|
+
custom[key] = { ...custom[key], ...strings };
|
|
49
|
+
for (const fn of listeners) fn();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The keys and texts built in for a language ('en' or 'de'), for an app that fills in its own. */
|
|
53
|
+
export function builtInStrings(lang = 'en') {
|
|
54
|
+
return { ...(BUILT_IN[String(lang).toLowerCase().split('-')[0]] || BUILT_IN.en) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Called after every setStrings(); base.js hands the change on to the elements. */
|
|
58
|
+
export function onStringsChange(fn) { listeners.add(fn); }
|
|
59
|
+
|
|
60
|
+
/** The language of `el`: its nearest lang attribute, across shadow roots, else the page's; the primary subtag. */
|
|
61
|
+
export function langOf(el) {
|
|
62
|
+
for (let node = el; node; node = node.getRootNode?.().host) {
|
|
63
|
+
const hit = node.closest?.('[lang]');
|
|
64
|
+
if (hit) return hit.getAttribute('lang').toLowerCase().split('-')[0];
|
|
65
|
+
}
|
|
66
|
+
return (document.documentElement.lang || 'en').toLowerCase().split('-')[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The text `key` for `el`: its language's own set by setStrings(), then the one for every language, the built-in one, English. */
|
|
70
|
+
export function str(key, el) {
|
|
71
|
+
const lang = langOf(el);
|
|
72
|
+
return custom[lang]?.[key] ?? custom['*']?.[key] ?? BUILT_IN[lang]?.[key] ?? BUILT_IN.en[key] ?? key;
|
|
73
|
+
}
|