@oscarpalmer/oui 0.34.0 → 0.35.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.
Files changed (32) hide show
  1. package/dist/js/combobox/constants.combobox.d.mts +1 -1
  2. package/dist/js/combobox/data.combobox.d.mts +1 -1
  3. package/dist/js/combobox/data.combobox.mjs +1 -1
  4. package/dist/js/combobox/dom.combobox.d.mts +1 -1
  5. package/dist/js/combobox/dom.combobox.mjs +1 -1
  6. package/dist/js/combobox/event.combobox.d.mts +1 -1
  7. package/dist/js/combobox/event.combobox.mjs +1 -1
  8. package/dist/js/combobox/form.combobox.d.mts +1 -1
  9. package/dist/js/combobox/form.combobox.mjs +1 -1
  10. package/dist/js/combobox/index.d.mts +1 -1
  11. package/dist/js/combobox/index.mjs +3 -2
  12. package/dist/js/combobox/misc.combobox.d.mts +2 -14
  13. package/dist/js/combobox/misc.combobox.mjs +2 -2
  14. package/dist/js/combobox/models.combobox.d.mts +1 -1
  15. package/dist/js/combobox/state.combobox.d.mts +4 -2
  16. package/dist/js/combobox/state.combobox.mjs +2 -2
  17. package/dist/js/{constants.combobox-DqNUGBLS.d.mts → constants.combobox-h_kyDegw.d.mts} +12 -7
  18. package/dist/js/{data.combobox-8RwBv5ey.mjs → data.combobox-B8HMkDOh.mjs} +245 -292
  19. package/dist/js/index.d.mts +1 -1
  20. package/dist/js/index.mjs +1 -1
  21. package/dist/js/l10n.d.mts +20 -0
  22. package/dist/js/l10n.mjs +80 -0
  23. package/package.json +1 -1
  24. package/src/js/combobox/dom.combobox.ts +57 -26
  25. package/src/js/combobox/form.combobox.ts +26 -25
  26. package/src/js/combobox/index.ts +10 -4
  27. package/src/js/combobox/misc.combobox.ts +50 -0
  28. package/src/js/combobox/state.combobox.ts +13 -1
  29. package/src/js/l10n.ts +192 -0
  30. package/dist/js/combobox/i18n.combobox.d.mts +0 -2
  31. package/dist/js/combobox/i18n.combobox.mjs +0 -2
  32. package/src/js/combobox/i18n.combobox.ts +0 -179
@@ -0,0 +1,20 @@
1
+ import { GenericCallback } from "@oscarpalmer/atoms/models";
2
+ //#region src/js/l10n.d.ts
3
+ type LocalizationParameters<Localization> = Localization extends GenericCallback ? Parameters<Localization> : never;
4
+ type OuiLocalizationValues = Record<string, string | GenericCallback>;
5
+ declare class OuiLocalizations<Localizations extends OuiLocalizationValues> {
6
+ readonly defaults: Localizations;
7
+ readonly keys: Set<keyof Localizations>;
8
+ readonly values: Record<string, Partial<Localizations>>;
9
+ constructor(defaults: Localizations);
10
+ get<Key extends keyof Localizations>(element: Element, key: Key, localizations: Record<string, Partial<Localizations>>): string | GenericCallback;
11
+ set(language: string, localizations: Partial<Localizations>): void;
12
+ set(localizations: Record<string, Partial<Localizations>>): void;
13
+ }
14
+ declare function getLanguage(element: Element): string;
15
+ declare function getLocalization<Localizations extends OuiLocalizationValues, Key extends keyof Localizations>(localizations: OuiLocalizations<Localizations>, element: Element, key: Key, value: Record<string, Partial<Localizations>>): Localizations[Key] | GenericCallback;
16
+ declare function getLocalizations<Localizations extends OuiLocalizationValues>(defaults: Localizations): OuiLocalizations<Localizations>;
17
+ declare function localize<Localizations extends OuiLocalizationValues, Key extends keyof Localizations, Localization extends Localizations[Key]>(localization: Localization, data: Record<string, unknown>, parameters: LocalizationParameters<Localization>): string;
18
+ declare function updateLocalizations<Localizations extends OuiLocalizationValues>(instance: OuiLocalizations<Localizations>, values: Record<string, Partial<Localizations>>, destination?: Record<string, Partial<Localizations>>, onAfter?: () => void): void;
19
+ //#endregion
20
+ export { OuiLocalizations, getLanguage, getLocalization, getLocalizations, localize, updateLocalizations };
@@ -0,0 +1,80 @@
1
+ import { getString } from "@oscarpalmer/atoms/string";
2
+ //#region src/js/l10n.ts
3
+ var OuiLocalizations = class {
4
+ defaults;
5
+ keys;
6
+ values = {};
7
+ constructor(defaults) {
8
+ this.defaults = defaults;
9
+ this.keys = new Set(Object.keys(defaults));
10
+ }
11
+ get(element, key, localizations) {
12
+ return getLocalization(this, element, key, localizations);
13
+ }
14
+ set(first, second) {
15
+ updateLocalizations(this, typeof first === "string" ? { [first]: second } : first, this.values);
16
+ }
17
+ };
18
+ function getLanguage(element) {
19
+ return (element.closest("[lang]")?.getAttribute("lang") ?? document.documentElement.lang).toLowerCase();
20
+ }
21
+ function getLocalization(localizations, element, key, value) {
22
+ const language = getLanguage(element);
23
+ const values = [value, localizations.values];
24
+ const { length } = values;
25
+ let localization;
26
+ let shortLanguage;
27
+ for (let index = 0; index < length; index += 1) {
28
+ const value = values[index];
29
+ localization = value?.[language]?.[key];
30
+ if (localization != null) break;
31
+ shortLanguage ??= language.split("-")[0];
32
+ if (shortLanguage === language) continue;
33
+ localization = value?.[shortLanguage]?.[key];
34
+ if (localization != null) break;
35
+ }
36
+ return localization ?? localizations.defaults[key];
37
+ }
38
+ function getLocalizations(defaults) {
39
+ return new OuiLocalizations(defaults);
40
+ }
41
+ function localize(localization, data, parameters) {
42
+ if (typeof localization === "function") return localization(...parameters);
43
+ const keys = Object.keys(data);
44
+ const { length } = keys;
45
+ let localized = localization;
46
+ for (let index = 0; index < length; index += 1) {
47
+ const key = keys[index];
48
+ const value = data[key];
49
+ localized = localized.replaceAll(`{${key}}`, getString(value));
50
+ }
51
+ return localized;
52
+ }
53
+ function updateLocalizations(instance, values, destination, onAfter) {
54
+ if (typeof values !== "object" || values == null) return;
55
+ const localizations = destination ?? instance.values;
56
+ const languages = Object.keys(values);
57
+ const languagesLength = languages.length;
58
+ let updated = false;
59
+ for (let languagesIndex = 0; languagesIndex < languagesLength; languagesIndex += 1) {
60
+ const language = languages[languagesIndex];
61
+ const languageValue = values[language];
62
+ if (typeof languageValue !== "object" || languageValue == null) continue;
63
+ const normalizedLanguage = language.toLowerCase();
64
+ localizations[normalizedLanguage] ??= {};
65
+ const keys = Object.keys(languageValue);
66
+ const keysLength = keys.length;
67
+ for (let keysIndex = 0; keysIndex < keysLength; keysIndex += 1) {
68
+ const key = keys[keysIndex];
69
+ const keyValue = languageValue[key];
70
+ const normalizedKey = key.toLowerCase();
71
+ if (instance.keys.has(normalizedKey) && (typeof keyValue === "string" || typeof keyValue === "function")) {
72
+ updated = true;
73
+ localizations[normalizedLanguage][normalizedKey] = keyValue;
74
+ }
75
+ }
76
+ }
77
+ if (updated && typeof onAfter === "function") onAfter();
78
+ }
79
+ //#endregion
80
+ export { OuiLocalizations, getLanguage, getLocalization, getLocalizations, localize, updateLocalizations };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -18,7 +18,6 @@ import {
18
18
  comboboxScrollOptions,
19
19
  } from './constants.combobox';
20
20
  import {updateComboboxValidity} from './form.combobox';
21
- import {getComboboxLocalization} from './i18n.combobox';
22
21
  import type {
23
22
  OuiComboboxAutoComplete,
24
23
  OuiComboboxAutoCompleteType,
@@ -29,6 +28,8 @@ import type {
29
28
  OuiComboboxState,
30
29
  OuiComboboxType,
31
30
  } from './models.combobox';
31
+ import {getLocalization, localize} from '../l10n';
32
+ import {comboboxLocalizations} from './state.combobox';
32
33
 
33
34
  // #region Types
34
35
 
@@ -471,17 +472,26 @@ export function createComboboxSelectionItem(
471
472
  option: HTMLElement,
472
473
  state?: OuiComboboxState,
473
474
  ): OuiComboboxSelectionItem {
475
+ const optionLabel = option.textContent;
476
+ const optionValue = getData(option, 'value', false) ?? '';
477
+
474
478
  const element = createElement('li');
475
479
 
476
480
  const localization =
477
- state == null ? option.textContent : getComboboxLocalization('remove', state);
481
+ state == null
482
+ ? option.textContent
483
+ : getLocalization(comboboxLocalizations, state.host, 'remove', state.localizations);
478
484
 
479
485
  const button = createElement('button', {
480
486
  aria: {
481
- label:
482
- typeof localization === 'function'
483
- ? localization(option.textContent ?? '', getData(option, 'value', false) ?? '')
484
- : localization,
487
+ label: localize(
488
+ localization,
489
+ {
490
+ label: optionLabel,
491
+ value: optionValue,
492
+ },
493
+ [optionLabel, optionValue],
494
+ ),
485
495
  },
486
496
  attribute: {
487
497
  [COMBOBOX_ATTRIBUTE_SELECTION_ITEM]: '',
@@ -598,15 +608,16 @@ export function updateComboboxAnchor(state: OuiComboboxState): void {
598
608
  return;
599
609
  }
600
610
 
601
- const localization = getComboboxLocalization('selected', state);
602
-
603
- setAria(
604
- state.anchor.element,
605
- 'label',
606
- typeof localization === 'function'
607
- ? localization(state.options.selected.length)
608
- : localization.replace('{amount}', state.options.selected.length.toString()),
611
+ const localization = getLocalization(
612
+ comboboxLocalizations,
613
+ state.host,
614
+ 'selected',
615
+ state.localizations,
609
616
  );
617
+
618
+ const {length} = state.options.selected;
619
+
620
+ setAria(state.anchor.element, 'label', localize(localization, {amount: length}, [length]));
610
621
  }
611
622
 
612
623
  export function updateComboboxAutocomplete(
@@ -648,16 +659,23 @@ export function updateComboboxMessage(state: OuiComboboxState, visible: number):
648
659
  setAttribute(state.message, COMBOBOX_ATTRIBUTE_HIDDEN, visible > 0 ? '' : undefined);
649
660
 
650
661
  if (visible === 0) {
651
- const localization = getComboboxLocalization('empty', state);
662
+ const localization = getLocalization(
663
+ comboboxLocalizations,
664
+ state.host,
665
+ 'empty',
666
+ state.localizations,
667
+ );
652
668
 
653
- state.message.textContent = typeof localization === 'function' ? localization() : localization;
669
+ state.message.textContent = localize(localization, {}, []);
654
670
  } else {
655
- const localization = getComboboxLocalization('results', state);
671
+ const localization = getLocalization(
672
+ comboboxLocalizations,
673
+ state.host,
674
+ 'results',
675
+ state.localizations,
676
+ );
656
677
 
657
- state.message.textContent =
658
- typeof localization === 'function'
659
- ? localization(visible)
660
- : localization.replace('{amount}', visible.toString());
678
+ state.message.textContent = localize(localization, {amount: visible}, [visible]);
661
679
  }
662
680
 
663
681
  state.message.hidden = false;
@@ -701,7 +719,7 @@ export function updateComboboxSelection(
701
719
 
702
720
  let options = active == null ? [] : [active];
703
721
 
704
- if (selected != null) {
722
+ if (added && selected != null) {
705
723
  options = selected;
706
724
 
707
725
  const {length} = state.selection.items;
@@ -766,7 +784,12 @@ export function updateComboboxSelectionItems(state: OuiComboboxState): void {
766
784
  return;
767
785
  }
768
786
 
769
- const localization = getComboboxLocalization('remove', state);
787
+ const localization = getLocalization(
788
+ comboboxLocalizations,
789
+ state.host,
790
+ 'remove',
791
+ state.localizations,
792
+ );
770
793
 
771
794
  const {length} = state.selection.items;
772
795
 
@@ -781,12 +804,20 @@ export function updateComboboxSelectionItems(state: OuiComboboxState): void {
781
804
  continue;
782
805
  }
783
806
 
807
+ const optionLabel = option.textContent;
808
+ const optionValue = getData(option, 'value', false) ?? '';
809
+
784
810
  setAria(
785
811
  item.button,
786
812
  'label',
787
- typeof localization === 'function'
788
- ? localization(option?.textContent ?? '', getData(option, 'value', false) ?? '')
789
- : localization.replace('{label}', option?.textContent ?? ''),
813
+ localize(
814
+ localization,
815
+ {
816
+ label: optionLabel,
817
+ value: optionValue,
818
+ },
819
+ [optionLabel, optionValue],
820
+ ),
790
821
  );
791
822
  }
792
823
  }
@@ -1,7 +1,8 @@
1
1
  import {dispatch} from '@oscarpalmer/toretto/event';
2
+ import {getLocalization, localize} from '../l10n';
2
3
  import {updateComboboxAnchor} from './dom.combobox';
3
- import {getComboboxLocalization} from './i18n.combobox';
4
4
  import type {OuiComboboxState} from './models.combobox';
5
+ import {comboboxLocalizations} from './state.combobox';
5
6
 
6
7
  // #region Functions
7
8
 
@@ -40,28 +41,29 @@ export function updateComboboxValidity(state: OuiComboboxState): void {
40
41
  if (state.autocomplete.none) {
41
42
  const valueMissing = state.required && state.filter.element.value.length === 0;
42
43
 
43
- const localization = getComboboxLocalization('invalid', state);
44
-
45
- state.internals.setValidity(
46
- {valueMissing},
47
- typeof localization === 'function' ? localization() : localization,
44
+ const localization = getLocalization(
45
+ comboboxLocalizations,
46
+ state.host,
47
+ 'invalid',
48
+ state.localizations,
48
49
  );
49
50
 
51
+ state.internals.setValidity({valueMissing}, localize(localization, {}, []));
52
+
50
53
  state.internals.checkValidity();
51
54
 
52
55
  return;
53
56
  }
54
57
 
58
+ let data: Record<string, unknown> = {};
55
59
  let error: 'badInput' | 'rangeOverflow' | 'rangeUnderflow' | undefined;
56
- let message: string | undefined;
60
+ let key: 'input' | 'max' | 'min' | undefined;
61
+ let parameters: unknown[] = [];
57
62
 
58
63
  switch (true) {
59
64
  case state.required && state.options.selected.length === 0: {
60
65
  error = 'badInput';
61
-
62
- const localization = getComboboxLocalization('input', state);
63
-
64
- message = typeof localization === 'function' ? localization() : localization;
66
+ key = 'input';
65
67
 
66
68
  break;
67
69
  }
@@ -69,14 +71,10 @@ export function updateComboboxValidity(state: OuiComboboxState): void {
69
71
  case state.multiple &&
70
72
  typeof state.max === 'number' &&
71
73
  state.options.selected.length > state.max: {
74
+ data = {amount: state.max};
72
75
  error = 'rangeOverflow';
73
-
74
- const localization = getComboboxLocalization('max', state);
75
-
76
- message =
77
- typeof localization === 'function'
78
- ? localization(state.max)
79
- : localization.replace('{max}', state.max.toString());
76
+ key = 'max';
77
+ parameters = [state.max];
80
78
 
81
79
  break;
82
80
  }
@@ -84,14 +82,10 @@ export function updateComboboxValidity(state: OuiComboboxState): void {
84
82
  case state.multiple &&
85
83
  typeof state.min === 'number' &&
86
84
  state.options.selected.length < state.min: {
85
+ data = {amount: state.min};
87
86
  error = 'rangeUnderflow';
88
-
89
- const localization = getComboboxLocalization('min', state);
90
-
91
- message =
92
- typeof localization === 'function'
93
- ? localization(state.min)
94
- : localization.replace('{min}', state.min.toString());
87
+ key = 'min';
88
+ parameters = [state.min];
95
89
 
96
90
  break;
97
91
  }
@@ -100,6 +94,13 @@ export function updateComboboxValidity(state: OuiComboboxState): void {
100
94
  break;
101
95
  }
102
96
 
97
+ const localization =
98
+ key == null
99
+ ? undefined
100
+ : getLocalization(comboboxLocalizations, state.host, key, state.localizations);
101
+
102
+ const message = localization == null ? undefined : localize(localization, data, parameters);
103
+
103
104
  state.internals.setValidity(
104
105
  error == null
105
106
  ? {}
@@ -1,12 +1,17 @@
1
1
  import {parse} from '@oscarpalmer/atoms/string';
2
2
  import {getAttribute} from '@oscarpalmer/toretto/attribute';
3
+ import {updateLocalizations} from '../l10n';
3
4
  import {COMBOBOX_TAGNAME} from './constants.combobox';
4
5
  import {setComboboxValue, setComboboxValues} from './data.combobox';
5
6
  import {setComboboxDisabled, setComboboxReadonly, setComboboxRequired} from './dom.combobox';
6
- import {updateComboboxLocalizations} from './i18n.combobox';
7
7
  import {setComboboxFilter, setComboboxLimit} from './misc.combobox';
8
8
  import type {OuiComboboxLocalizations, OuiComboboxState, OuiComboboxType} from './models.combobox';
9
- import {attachCombobox, createComboboxState, detachCombobox} from './state.combobox';
9
+ import {
10
+ attachCombobox,
11
+ comboboxLocalizations,
12
+ createComboboxState,
13
+ detachCombobox,
14
+ } from './state.combobox';
10
15
 
11
16
  // #region Types
12
17
 
@@ -269,7 +274,8 @@ export class OuiComboboxElement extends HTMLElement {
269
274
  first: string | Record<string, Partial<OuiComboboxLocalizations>>,
270
275
  second?: Partial<OuiComboboxLocalizations>,
271
276
  ): void {
272
- updateComboboxLocalizations(
277
+ updateLocalizations(
278
+ comboboxLocalizations,
273
279
  typeof first === 'string'
274
280
  ? {
275
281
  [first]: second!,
@@ -302,6 +308,6 @@ customElements.define(COMBOBOX_TAGNAME, OuiComboboxElement);
302
308
 
303
309
  // #region Exports
304
310
 
305
- export {setComboboxLocalizations} from './i18n.combobox';
311
+ export {setComboboxLocalizations} from './misc.combobox';
306
312
 
307
313
  // #endregion
@@ -2,14 +2,22 @@ import {getAria} from '@oscarpalmer/toretto/aria';
2
2
  import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
3
3
  import type {AnyAriaBooleanAttribute} from '@oscarpalmer/toretto/models';
4
4
  import type {OuiComboboxElement} from '.';
5
+ import {updateLocalizations} from '../l10n';
5
6
  import {COMBOBOX_AUTOCOMPLETES} from './constants.combobox';
7
+ import {
8
+ updateComboboxAnchor,
9
+ updateComboboxMessage,
10
+ updateComboboxSelectionItems,
11
+ } from './dom.combobox';
6
12
  import {onComboboxFilter} from './event.combobox';
7
13
  import {updateComboboxValidity} from './form.combobox';
8
14
  import type {
9
15
  OuiComboboxAutoComplete,
10
16
  OuiComboboxAutoCompleteType,
17
+ OuiComboboxLocalizations,
11
18
  OuiComboboxState,
12
19
  } from './models.combobox';
20
+ import {comboboxLocalizations, comboboxStates} from './state.combobox';
13
21
 
14
22
  // #region Functions
15
23
 
@@ -129,6 +137,48 @@ export function setComboboxLimit(
129
137
  }
130
138
  }
131
139
 
140
+ /**
141
+ * Set combobox localizations for a specific language
142
+ *
143
+ * @param language Language code
144
+ * @param localizations Localizations for language
145
+ */
146
+ export function setComboboxLocalizations(
147
+ language: string,
148
+ localizations: Partial<OuiComboboxLocalizations>,
149
+ ): void;
150
+
151
+ /**
152
+ * Set combobox localizations for one or more languages
153
+ *
154
+ * @param localizations Localizations for one or more languages
155
+ */
156
+ export function setComboboxLocalizations(
157
+ localizations: Record<string, Partial<OuiComboboxLocalizations>>,
158
+ ): void;
159
+
160
+ export function setComboboxLocalizations(
161
+ first: string | Record<string, Partial<OuiComboboxLocalizations>>,
162
+ second?: Partial<OuiComboboxLocalizations>,
163
+ ): void {
164
+ updateLocalizations(
165
+ comboboxLocalizations,
166
+ typeof first === 'string'
167
+ ? {
168
+ [first]: second!,
169
+ }
170
+ : first,
171
+ comboboxLocalizations.values,
172
+ () => {
173
+ for (const state of comboboxStates.set) {
174
+ updateComboboxAnchor(state);
175
+ updateComboboxMessage(state, getComboboxVisibleOptions(state).length);
176
+ updateComboboxSelectionItems(state);
177
+ }
178
+ },
179
+ );
180
+ }
181
+
132
182
  // #endregion
133
183
 
134
184
  // #region Variables
@@ -3,6 +3,7 @@ import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
3
3
  import {getData} from '@oscarpalmer/toretto/data';
4
4
  import {on} from '@oscarpalmer/toretto/event';
5
5
  import {createEmbeddedFloatable} from '../floatable/floatable.embedded';
6
+ import {getLocalizations} from '../l10n';
6
7
  import {
7
8
  COMBOBOX_MESSAGE_LABEL,
8
9
  COMBOBOX_MESSAGE_SELECT,
@@ -31,7 +32,7 @@ import {
31
32
  getComboboxVisibleOptions,
32
33
  setComboboxLimit,
33
34
  } from './misc.combobox';
34
- import type {OuiComboboxState} from './models.combobox';
35
+ import type {OuiComboboxLocalizations, OuiComboboxState} from './models.combobox';
35
36
 
36
37
  // #region Functions
37
38
 
@@ -227,6 +228,17 @@ export function detachCombobox(state: OuiComboboxState): void {
227
228
 
228
229
  // #region Variables
229
230
 
231
+ export const comboboxLocalizations = getLocalizations<OuiComboboxLocalizations>({
232
+ empty: 'No results found',
233
+ input: 'Please enter a value',
234
+ invalid: 'Please select an option from the list',
235
+ max: max => `Please select fewer options (maximum is ${max})`,
236
+ min: min => `Please select more options (minimum is ${min})`,
237
+ remove: label => `Remove ${label}`,
238
+ results: amount => `${amount} results available`,
239
+ selected: amount => `${amount} selected`,
240
+ });
241
+
230
242
  export const comboboxStates = {
231
243
  map: new WeakMap<HTMLElement, OuiComboboxState>(),
232
244
  set: new Set<OuiComboboxState>(),
package/src/js/l10n.ts ADDED
@@ -0,0 +1,192 @@
1
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import {getString} from '@oscarpalmer/atoms/string';
3
+
4
+ // #region Types
5
+
6
+ type LocalizationParameters<Localization> = Localization extends GenericCallback
7
+ ? Parameters<Localization>
8
+ : never;
9
+
10
+ type OuiLocalizationValues = Record<string, string | GenericCallback>;
11
+
12
+ export class OuiLocalizations<Localizations extends OuiLocalizationValues> {
13
+ readonly defaults: Localizations;
14
+
15
+ readonly keys: Set<keyof Localizations>;
16
+
17
+ readonly values: Record<string, Partial<Localizations>> = {};
18
+
19
+ constructor(defaults: Localizations) {
20
+ this.defaults = defaults;
21
+ this.keys = new Set(Object.keys(defaults) as Array<keyof Localizations>);
22
+ }
23
+
24
+ get<Key extends keyof Localizations>(
25
+ element: Element,
26
+ key: Key,
27
+ localizations: Record<string, Partial<Localizations>>,
28
+ ): string | GenericCallback {
29
+ return getLocalization(this, element, key, localizations);
30
+ }
31
+
32
+ set(language: string, localizations: Partial<Localizations>): void;
33
+
34
+ set(localizations: Record<string, Partial<Localizations>>): void;
35
+
36
+ set(
37
+ first: string | Record<string, Partial<Localizations>>,
38
+ second?: Partial<Localizations>,
39
+ ): void {
40
+ updateLocalizations(
41
+ this,
42
+ typeof first === 'string'
43
+ ? {
44
+ [first]: second!,
45
+ }
46
+ : first,
47
+ this.values,
48
+ );
49
+ }
50
+ }
51
+
52
+ // #endregion
53
+
54
+ // #region Functions
55
+
56
+ export function getLanguage(element: Element): string {
57
+ return (
58
+ element.closest('[lang]')?.getAttribute('lang') ?? document.documentElement.lang
59
+ ).toLowerCase();
60
+ }
61
+
62
+ export function getLocalization<
63
+ Localizations extends OuiLocalizationValues,
64
+ Key extends keyof Localizations,
65
+ >(
66
+ localizations: OuiLocalizations<Localizations>,
67
+ element: Element,
68
+ key: Key,
69
+ value: Record<string, Partial<Localizations>>,
70
+ ): Localizations[Key] | GenericCallback {
71
+ const language = getLanguage(element);
72
+
73
+ const values = [value, localizations.values];
74
+ const {length} = values;
75
+
76
+ let localization: Localizations[Key] | undefined;
77
+ let shortLanguage: string | undefined;
78
+
79
+ for (let index = 0; index < length; index += 1) {
80
+ const value = values[index];
81
+
82
+ localization = value?.[language]?.[key];
83
+
84
+ if (localization != null) {
85
+ break;
86
+ }
87
+
88
+ shortLanguage ??= language.split('-')[0];
89
+
90
+ if (shortLanguage === language) {
91
+ continue;
92
+ }
93
+
94
+ localization = value?.[shortLanguage]?.[key];
95
+
96
+ if (localization != null) {
97
+ break;
98
+ }
99
+ }
100
+
101
+ return localization ?? localizations.defaults[key];
102
+ }
103
+
104
+ export function getLocalizations<Localizations extends OuiLocalizationValues>(
105
+ defaults: Localizations,
106
+ ): OuiLocalizations<Localizations> {
107
+ return new OuiLocalizations(defaults);
108
+ }
109
+
110
+ export function localize<
111
+ Localizations extends OuiLocalizationValues,
112
+ Key extends keyof Localizations,
113
+ Localization extends Localizations[Key],
114
+ >(
115
+ localization: Localization,
116
+ data: Record<string, unknown>,
117
+ parameters: LocalizationParameters<Localization>,
118
+ ): string {
119
+ if (typeof localization === 'function') {
120
+ return localization(...parameters);
121
+ }
122
+
123
+ const keys = Object.keys(data);
124
+ const {length} = keys;
125
+
126
+ let localized = localization as string;
127
+
128
+ for (let index = 0; index < length; index += 1) {
129
+ const key = keys[index];
130
+ const value = data[key];
131
+
132
+ localized = localized.replaceAll(`{${key}}`, getString(value));
133
+ }
134
+
135
+ return localized;
136
+ }
137
+
138
+ export function updateLocalizations<Localizations extends OuiLocalizationValues>(
139
+ instance: OuiLocalizations<Localizations>,
140
+ values: Record<string, Partial<Localizations>>,
141
+ destination?: Record<string, Partial<Localizations>>,
142
+ onAfter?: () => void,
143
+ ): void {
144
+ if (typeof values !== 'object' || values == null) {
145
+ return;
146
+ }
147
+
148
+ const localizations = destination ?? instance.values;
149
+
150
+ const languages = Object.keys(values);
151
+ const languagesLength = languages.length;
152
+
153
+ let updated = false;
154
+
155
+ for (let languagesIndex = 0; languagesIndex < languagesLength; languagesIndex += 1) {
156
+ const language = languages[languagesIndex];
157
+ const languageValue = values[language];
158
+
159
+ if (typeof languageValue !== 'object' || languageValue == null) {
160
+ continue;
161
+ }
162
+
163
+ const normalizedLanguage = language.toLowerCase();
164
+
165
+ localizations[normalizedLanguage] ??= {};
166
+
167
+ const keys = Object.keys(languageValue);
168
+ const keysLength = keys.length;
169
+
170
+ for (let keysIndex = 0; keysIndex < keysLength; keysIndex += 1) {
171
+ const key = keys[keysIndex];
172
+ const keyValue = languageValue[key];
173
+
174
+ const normalizedKey = key.toLowerCase() as keyof Localizations;
175
+
176
+ if (
177
+ instance.keys.has(normalizedKey) &&
178
+ (typeof keyValue === 'string' || typeof keyValue === 'function')
179
+ ) {
180
+ updated = true;
181
+
182
+ localizations[normalizedLanguage][normalizedKey] = keyValue as never;
183
+ }
184
+ }
185
+ }
186
+
187
+ if (updated && typeof onAfter === 'function') {
188
+ onAfter();
189
+ }
190
+ }
191
+
192
+ // #endregion