@astryxdesign/core 0.5.1 → 0.5.2

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 (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/i18n/InternationalizationContext.d.ts +2 -0
  3. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  4. package/dist/i18n/InternationalizationContext.js +3 -1
  5. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  6. package/dist/i18n/InternationalizationProvider.js +3 -1
  7. package/dist/i18n/resolve.d.ts +1 -1
  8. package/dist/i18n/resolve.d.ts.map +1 -1
  9. package/dist/i18n/resolve.js +37 -38
  10. package/dist/i18n/useTranslator.d.ts.map +1 -1
  11. package/dist/i18n/useTranslator.js +2 -3
  12. package/package.json +2 -2
  13. package/src/Calendar/Calendar.doc.mjs +1 -1
  14. package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
  15. package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
  16. package/src/ComplexSelector/ComplexSelector.doc.mjs +2 -2
  17. package/src/DateInput/DateInput.doc.mjs +1 -1
  18. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  19. package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
  20. package/src/Field/Field.doc.mjs +1 -1
  21. package/src/FieldStatus/FieldStatus.doc.mjs +1 -1
  22. package/src/FileInput/FileInput.doc.mjs +1 -1
  23. package/src/Indicator/Indicator.doc.mjs +1 -1
  24. package/src/InputGroup/InputGroup.doc.mjs +1 -1
  25. package/src/MultiSelector/MultiSelector.doc.mjs +1 -1
  26. package/src/NumberInput/NumberInput.doc.mjs +1 -1
  27. package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
  28. package/src/RadioList/RadioList.doc.mjs +1 -1
  29. package/src/Selector/Selector.doc.mjs +1 -1
  30. package/src/Slider/Slider.doc.mjs +2 -2
  31. package/src/Switch/Switch.doc.mjs +1 -1
  32. package/src/TextArea/TextArea.doc.mjs +1 -1
  33. package/src/TextInput/TextInput.doc.mjs +1 -1
  34. package/src/TimeInput/TimeInput.doc.mjs +1 -1
  35. package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
  36. package/src/Typeahead/Typeahead.doc.mjs +1 -1
  37. package/src/i18n/InternationalizationContext.ts +3 -0
  38. package/src/i18n/InternationalizationProvider.tsx +8 -1
  39. package/src/i18n/__tests__/resolve.test.ts +23 -1
  40. package/src/i18n/resolve.ts +46 -49
  41. package/src/i18n/useTranslator.ts +2 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @xds/core
2
2
 
3
+ # 0.5.2
4
+
5
+ #### Fixes
6
+
7
+ - Rename the Data Input component category to Form Controls (#5686)
8
+
9
+ #### Performance
10
+
11
+ - Improve frequent translation lookups by pre-building locale maps (#4696)
12
+
13
+ #### Contributors
14
+
15
+ Thanks to everyone who contributed to this release:
16
+
17
+ - @ngolin
18
+ - @rubyycheung
19
+
20
+ ---
21
+
3
22
  # 0.5.1
4
23
 
5
24
  #### New Features
@@ -1,9 +1,11 @@
1
+ import { getResolve } from './resolve';
1
2
  import type { Locale, MessagesByLocale, Overrides } from './types';
2
3
  export interface InternationalizationContextValue {
3
4
  locale: Locale;
4
5
  direction: 'ltr' | 'rtl';
5
6
  messages: MessagesByLocale;
6
7
  overrides?: Overrides;
8
+ translate: ReturnType<typeof getResolve>;
7
9
  }
8
10
  /**
9
11
  * Default value falls through to the shipped en catalog in resolve().
@@ -1 +1 @@
1
- {"version":3,"file":"InternationalizationContext.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationContext.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEjE,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,2DAKpC,CAAC"}
1
+ {"version":3,"file":"InternationalizationContext.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationContext.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAC,UAAU,EAAC,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEjE,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;CAC1C;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,2DAMpC,CAAC"}
@@ -22,6 +22,7 @@
22
22
  * - /packages/core/src/i18n/index.ts
23
23
  */
24
24
  import { createContext } from 'react';
25
+ import { getResolve } from "./resolve.js";
25
26
  /**
26
27
  * Default value falls through to the shipped en catalog in resolve().
27
28
  * A consumer that doesn't render a provider still gets English defaults.
@@ -29,6 +30,7 @@ import { createContext } from 'react';
29
30
  export const InternationalizationContext = /*#__PURE__*/createContext({
30
31
  locale: 'en',
31
32
  direction: 'ltr',
32
- messages: {}
33
+ messages: {},
34
+ translate: getResolve('en', {})
33
35
  });
34
36
  InternationalizationContext.displayName = 'InternationalizationContext';
@@ -1 +1 @@
1
- {"version":3,"file":"InternationalizationProvider.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationProvider.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG9C,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEjE,MAAM,WAAW,iCAAiC;IAChD;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,MAAM,EACN,QAAQ,EACR,SAAS,EACT,GAAG,EACH,QAAQ,GACT,EAAE,iCAAiC,+BAWnC;yBAjBe,4BAA4B"}
1
+ {"version":3,"file":"InternationalizationProvider.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationProvider.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG9C,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAGjE,MAAM,WAAW,iCAAiC;IAChD;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,MAAM,EACN,QAAQ,EACR,SAAS,EACT,GAAG,EACH,QAAQ,GACT,EAAE,iCAAiC,+BAiBnC;yBAvBe,4BAA4B"}
@@ -21,6 +21,7 @@
21
21
  import { useMemo } from 'react';
22
22
  import { InternationalizationContext } from "./InternationalizationContext.js";
23
23
  import { getLocaleDirection } from "./getLocaleDirection.js";
24
+ import { getResolve } from "./resolve.js";
24
25
  import { jsx as _jsx } from "react/jsx-runtime";
25
26
  /**
26
27
  * Provides locale + additional messages + overrides to all astryx
@@ -38,7 +39,8 @@ export function InternationalizationProvider({
38
39
  locale,
39
40
  direction,
40
41
  messages: messages ?? {},
41
- overrides
42
+ overrides,
43
+ translate: getResolve(locale, messages ?? {}, overrides)
42
44
  }), [locale, direction, messages, overrides]);
43
45
  return /*#__PURE__*/_jsx(InternationalizationContext, {
44
46
  value: value,
@@ -13,7 +13,7 @@ import type { Locale, MessagesByLocale, Overrides } from './types';
13
13
  * shipped en catalog separately as the final source-of-truth.
14
14
  */
15
15
  export declare function resolveLocaleChain(locale: Locale): Locale[];
16
- export declare function resolve(key: string, values: Record<string, unknown> | undefined, locale: Locale, messages: MessagesByLocale, overrides: Overrides | undefined): string;
16
+ export declare function getResolve(locale: Locale, messages: MessagesByLocale, overrides?: Overrides): (key: string, values: Record<string, unknown> | undefined) => string;
17
17
  /**
18
18
  * Reset internal caches. Test-only.
19
19
  * @internal
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/i18n/resolve.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAU,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAwB1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAc3D;AAsCD,wBAAgB,OAAO,CACrB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,SAAS,GAAG,SAAS,GAC/B,MAAM,CA0BR;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAGtC"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/i18n/resolve.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAU,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAwB1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAc3D;AAkCD,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,CAAC,EAAE,SAAS,IAIb,KAAK,MAAM,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,YA0BjE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAGtC"}
@@ -69,55 +69,54 @@ export function resolveLocaleChain(locale) {
69
69
  }
70
70
  return chain;
71
71
  }
72
- function lookup(key, locale, messages, overrides) {
72
+ function getLookup(locale, messages, overrides) {
73
+ const lookup = {};
73
74
  const chain = resolveLocaleChain(locale);
74
-
75
75
  // 1 + 2. Overrides (most specific to least specific in the chain)
76
76
  if (overrides !== undefined) {
77
77
  for (const tag of chain) {
78
- const value = overrides[tag]?.[key];
79
- if (value !== undefined) {
80
- return value;
78
+ if (overrides[tag]) {
79
+ for (const [key, value] of Object.entries(overrides[tag])) {
80
+ if (lookup[key] === undefined && value !== null) {
81
+ lookup[key] = value;
82
+ }
83
+ }
81
84
  }
82
85
  }
83
86
  }
84
-
85
- // 3 + 4. Shipped catalogs (most specific to least specific)
86
87
  for (const tag of chain) {
87
- const entry = messages[tag]?.[key];
88
- if (entry !== undefined) {
89
- return entry.defaultMessage;
88
+ if (messages[tag]) {
89
+ for (const [key, value] of Object.entries(messages[tag])) {
90
+ if (lookup[key] === undefined && value?.defaultMessage !== null) {
91
+ lookup[key] = value?.defaultMessage;
92
+ }
93
+ }
90
94
  }
91
95
  }
92
-
93
- // 5. Shipped en catalog — always present
94
- const enEntry = EN_CATALOG[key];
95
- if (enEntry !== undefined) {
96
- return enEntry.defaultMessage;
97
- }
98
-
99
- // 6. Nothing found
100
- return null;
96
+ return lookup;
101
97
  }
102
- export function resolve(key, values, locale, messages, overrides) {
103
- const result = lookup(key, locale, messages, overrides);
104
- if (result === null) {
105
- // Fires ONLY when a key is missing from every source including the
106
- // shipped `en` catalog — a real bug (typo, stale catalog, deleted key).
107
- // Fallback to `en` from a non-en locale is expected and stays silent,
108
- // matching the FormatJS / i18next default.
109
- warnOnce(`astryx-i18n:${locale}::${key}`, 'astryx-i18n', `missing key: ${key} (locale: ${locale})`);
110
- return key;
111
- }
112
- if (values === undefined) {
113
- // Static string — skip the parser entirely for the common case
114
- return result;
115
- }
116
- const formatted = getFormatter(result, locale).format(values);
117
- // IntlMessageFormat.format returns string | (string | React elements) — we
118
- // only ever pass string values so it will be a string; assert for the type
119
- // system.
120
- return formatted;
98
+ export function getResolve(locale, messages, overrides) {
99
+ const lookup = getLookup(locale, messages, overrides);
100
+ return (key, values) => {
101
+ const result = lookup[key] ?? EN_CATALOG[key]?.defaultMessage;
102
+ if (result === undefined) {
103
+ // Fires ONLY when a key is missing from every source including the
104
+ // shipped `en` catalog a real bug (typo, stale catalog, deleted key).
105
+ // Fallback to `en` from a non-en locale is expected and stays silent,
106
+ // matching the FormatJS / i18next default.
107
+ warnOnce(`astryx-i18n:${locale}::${key}`, 'astryx-i18n', `missing key: ${key} (locale: ${locale})`);
108
+ return key;
109
+ }
110
+ if (values === undefined) {
111
+ // Static string — skip the parser entirely for the common case
112
+ return result;
113
+ }
114
+ const formatted = getFormatter(result, locale).format(values);
115
+ // IntlMessageFormat.format returns string | (string | React elements) — we
116
+ // only ever pass string values so it will be a string; assert for the type
117
+ // system.
118
+ return formatted;
119
+ };
121
120
  }
122
121
 
123
122
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useTranslator.d.ts","sourceRoot":"","sources":["../../src/i18n/useTranslator.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI,YAAY,CAO5C"}
1
+ {"version":3,"file":"useTranslator.d.ts","sourceRoot":"","sources":["../../src/i18n/useTranslator.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI,YAAY,CAG5C"}
@@ -18,9 +18,8 @@
18
18
  * - /packages/core/src/i18n/index.ts
19
19
  * - /packages/core/src/i18n/t.client.ts
20
20
  */
21
- import { use, useCallback } from 'react';
21
+ import { use } from 'react';
22
22
  import { InternationalizationContext } from "./InternationalizationContext.js";
23
- import { resolve } from "./resolve.js";
24
23
  /**
25
24
  * Returns a translator function bound to the current provider's locale.
26
25
  * Safe to call from event handlers and effects.
@@ -35,5 +34,5 @@ import { resolve } from "./resolve.js";
35
34
  */
36
35
  export function useTranslator() {
37
36
  const ctx = use(InternationalizationContext);
38
- return useCallback((key, values) => resolve(key, values, ctx.locale, ctx.messages, ctx.overrides), [ctx.locale, ctx.messages, ctx.overrides]);
37
+ return ctx.translate;
39
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -669,7 +669,7 @@
669
669
  "@testing-library/dom": "^10.0.0",
670
670
  "@testing-library/jest-dom": "^6.6.0",
671
671
  "@testing-library/react": "^16.3.2",
672
- "@astryxdesign/cli": "0.5.1"
672
+ "@astryxdesign/cli": "0.5.2"
673
673
  },
674
674
  "dependencies": {
675
675
  "intl-messageformat": "^11.2.9"
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'Calendar',
7
7
  displayName: 'Calendar',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["calendar","datepicker","date picker","rangepicker","date range","monthview","daypicker"],
10
10
  usage: {
11
11
  description:
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'CheckboxInput',
7
7
  displayName: 'Checkbox Input',
8
8
  group: 'Checkbox',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: ["checkbox","check","toggle","tick","indeterminate","boolean","tristate"],
11
11
  props: [
12
12
  {
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'CheckboxList',
7
7
  displayName: 'Checkbox List',
8
8
  group: 'Checkbox',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  isHiddenFromOverview: true,
11
11
  keywords: ["checkboxlist","checkbox","checkboxgroup","multichoice","multiselect","checklist"],
12
12
  description: 'Checkbox group container with field integration for label, description, and status.',
@@ -15,7 +15,7 @@ export const docs = {
15
15
  name: 'ComplexSelector',
16
16
  displayName: 'Complex Selector',
17
17
  group: 'Selector',
18
- category: 'Data Input',
18
+ category: 'Form Controls',
19
19
  keywords: [
20
20
  'selector',
21
21
  'picker',
@@ -214,7 +214,7 @@ export const docsDense = {
214
214
  name: 'ComplexSelector',
215
215
  displayName: 'Complex Selector',
216
216
  group: 'Selector',
217
- category: 'Data Input',
217
+ category: 'Form Controls',
218
218
  description:
219
219
  'Input/ghost trigger + dialog-popover shell for rich custom selectors. Content gets value/onChange/close/state; content owns semantics. Use focus hooks and evaluate custom content against WCAG 2.2.',
220
220
  usage: {
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'DateInput',
7
7
  displayName: 'Date Input',
8
8
  group: 'DateInput',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'dateinput',
12
12
  'datepicker',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'DateRangeInput',
7
7
  displayName: 'Date Range Input',
8
8
  group: 'DateInput',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'daterangepicker',
12
12
  'daterange',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'DateTimeInput',
7
7
  displayName: 'Date Time Input',
8
8
  group: 'DateInput',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'datetimepicker',
12
12
  'datetime',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Field',
7
7
  displayName: 'Field',
8
8
  group: 'Field',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: ["field","formfield","formgroup","formcontrol","label","input","required","optional","helpertext","hint"],
11
11
  playground: {
12
12
  defaults: {
@@ -5,7 +5,7 @@ export const docs = {
5
5
  name: 'FieldStatus',
6
6
  displayName: 'Field Status',
7
7
  group: 'Field',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  isHiddenFromOverview: true,
10
10
  description:
11
11
  'Status message component for form field validation feedback. Messages are announced to screen readers through persistent live regions (assertive for errors, polite otherwise), so conditional mounting is safe.',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'FileInput',
7
7
  displayName: 'File Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["fileinput","file","upload","drag","drop","dropzone","attachment","browse"],
10
10
  props: [
11
11
  {
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Indicator',
7
7
  displayName: 'Indicator',
8
8
  group: 'Indicator',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  isHiddenFromOverview: true,
11
11
  keywords: [
12
12
  'indicator',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'InputGroup',
7
7
  displayName: 'Input Group',
8
8
  group: 'Field',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  isHiddenFromOverview: true,
11
11
  keywords: [
12
12
  'inputgroup',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'MultiSelector',
7
7
  displayName: 'Multi Selector',
8
8
  group: 'Selector',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'multiselect',
12
12
  'checkbox',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'NumberInput',
7
7
  displayName: 'Number Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'numberinput',
11
11
  'numberfield',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'PowerSearch',
7
7
  displayName: 'Power Search',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'powersearch',
11
11
  'search',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'RadioList',
7
7
  displayName: 'Radio List',
8
8
  group: 'Radio',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: ["radiolist","radio","radiogroup","radiobutton","optionlist","singlechoice","choicelist"],
11
11
  theming: {
12
12
  targets: [
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Selector',
7
7
  displayName: 'Selector',
8
8
  group: 'Selector',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'selector',
12
12
  'select',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'Slider',
7
7
  displayName: 'Slider',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["slider","range","slidebar","trackbar","scrubber","knob","thumb","rangeslider"],
10
10
  playground: {
11
11
  defaults: {
@@ -364,4 +364,4 @@ export const docsDense = {
364
364
  labelTooltip: 'Tooltip text for info icon next to label.',
365
365
  xstyle: 'StyleX layout styles; must be stylex.create() value.',
366
366
  },
367
- };
367
+ };
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'Switch',
7
7
  displayName: 'Switch',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'switch',
11
11
  'toggle',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'TextArea',
7
7
  displayName: 'Text Area',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["textarea","textfield","multiline","comment","message","autoresize","autosize","charlimit"],
10
10
  props: [
11
11
  {
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'TextInput',
7
7
  displayName: 'Text Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["textinput","textfield","input","search","clearable","prefix","suffix","adornment","validation"],
10
10
  props: [
11
11
  {
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'TimeInput',
7
7
  displayName: 'Time Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'timeinput',
11
11
  'timepicker',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'Tokenizer',
7
7
  displayName: 'Tokenizer',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["tokenizer","multiselect","multi-select","chips","tags","combobox","autocomplete","taginput","chipinput"],
10
10
  props: [
11
11
  {
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Typeahead',
7
7
  displayName: 'Typeahead',
8
8
  group: 'Typeahead',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'typeahead',
12
12
  'autocomplete',
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import {createContext} from 'react';
26
+ import {getResolve} from './resolve';
26
27
  import type {Locale, MessagesByLocale, Overrides} from './types';
27
28
 
28
29
  export interface InternationalizationContextValue {
@@ -30,6 +31,7 @@ export interface InternationalizationContextValue {
30
31
  direction: 'ltr' | 'rtl';
31
32
  messages: MessagesByLocale;
32
33
  overrides?: Overrides;
34
+ translate: ReturnType<typeof getResolve>;
33
35
  }
34
36
 
35
37
  /**
@@ -41,5 +43,6 @@ export const InternationalizationContext =
41
43
  locale: 'en',
42
44
  direction: 'ltr',
43
45
  messages: {},
46
+ translate: getResolve('en', {}),
44
47
  });
45
48
  InternationalizationContext.displayName = 'InternationalizationContext';
@@ -23,6 +23,7 @@ import {useMemo, type ReactNode} from 'react';
23
23
  import {InternationalizationContext} from './InternationalizationContext';
24
24
  import {getLocaleDirection} from './getLocaleDirection';
25
25
  import type {Locale, MessagesByLocale, Overrides} from './types';
26
+ import {getResolve} from './resolve';
26
27
 
27
28
  export interface InternationalizationProviderProps {
28
29
  /**
@@ -85,7 +86,13 @@ export function InternationalizationProvider({
85
86
  }: InternationalizationProviderProps) {
86
87
  const direction = dir ?? getLocaleDirection(locale);
87
88
  const value = useMemo(
88
- () => ({locale, direction, messages: messages ?? {}, overrides}),
89
+ () => ({
90
+ locale,
91
+ direction,
92
+ messages: messages ?? {},
93
+ overrides,
94
+ translate: getResolve(locale, messages ?? {}, overrides),
95
+ }),
89
96
  [locale, direction, messages, overrides],
90
97
  );
91
98
  return (
@@ -9,9 +9,16 @@
9
9
  */
10
10
 
11
11
  import {describe, expect, test, beforeEach, vi} from 'vitest';
12
- import {__resetForTests, resolve, resolveLocaleChain} from '../resolve';
12
+ import {__resetForTests, getResolve, resolveLocaleChain} from '../resolve';
13
13
  import type {Catalog, MessagesByLocale, Overrides} from '../types';
14
14
 
15
+ const resolve = (
16
+ ...[key, values, locale, messages, overrides]: [
17
+ ...Parameters<ReturnType<typeof getResolve>>,
18
+ ...Parameters<typeof getResolve>,
19
+ ]
20
+ ) => getResolve(locale, messages, overrides)(key, values);
21
+
15
22
  // Reset caches between tests so warn-once and formatter cache don't bleed.
16
23
  beforeEach(() => {
17
24
  __resetForTests();
@@ -209,6 +216,21 @@ describe('resolve — overrides', () => {
209
216
  // Should fall through to en since fr has no messages and es override doesn't apply
210
217
  expect(out).toBe('Go to next page');
211
218
  });
219
+
220
+ test('override `pt` should take precedence over message `pt-BR`', () => {
221
+ const overrides: Overrides = {
222
+ pt: {key: 'override'},
223
+ };
224
+ const messages: MessagesByLocale = {
225
+ 'pt-BR': {
226
+ key: {
227
+ defaultMessage: 'shipped',
228
+ },
229
+ },
230
+ };
231
+ const out = resolve('key', undefined, 'pt-BR', messages, overrides);
232
+ expect(out).toBe('override');
233
+ });
212
234
  });
213
235
 
214
236
  describe('resolve — ICU MessageFormat', () => {
@@ -73,74 +73,71 @@ export function resolveLocaleChain(locale: Locale): Locale[] {
73
73
  return chain;
74
74
  }
75
75
 
76
- function lookup(
77
- key: string,
76
+ function getLookup(
78
77
  locale: Locale,
79
78
  messages: MessagesByLocale,
80
- overrides: Overrides | undefined,
81
- ): string | null {
79
+ overrides?: Overrides,
80
+ ): Record<string, string> {
81
+ const lookup: Record<string, string> = {};
82
82
  const chain = resolveLocaleChain(locale);
83
-
84
83
  // 1 + 2. Overrides (most specific to least specific in the chain)
85
84
  if (overrides !== undefined) {
86
85
  for (const tag of chain) {
87
- const value = overrides[tag]?.[key];
88
- if (value !== undefined) {
89
- return value;
86
+ if (overrides[tag]) {
87
+ for (const [key, value] of Object.entries(overrides[tag])) {
88
+ if (lookup[key] === undefined && value !== null) {
89
+ lookup[key] = value;
90
+ }
91
+ }
90
92
  }
91
93
  }
92
94
  }
93
-
94
- // 3 + 4. Shipped catalogs (most specific to least specific)
95
95
  for (const tag of chain) {
96
- const entry = messages[tag]?.[key];
97
- if (entry !== undefined) {
98
- return entry.defaultMessage;
96
+ if (messages[tag]) {
97
+ for (const [key, value] of Object.entries(messages[tag])) {
98
+ if (lookup[key] === undefined && value?.defaultMessage !== null) {
99
+ lookup[key] = value?.defaultMessage;
100
+ }
101
+ }
99
102
  }
100
103
  }
101
104
 
102
- // 5. Shipped en catalog — always present
103
- const enEntry = EN_CATALOG[key];
104
- if (enEntry !== undefined) {
105
- return enEntry.defaultMessage;
106
- }
107
-
108
- // 6. Nothing found
109
- return null;
105
+ return lookup;
110
106
  }
111
107
 
112
- export function resolve(
113
- key: string,
114
- values: Record<string, unknown> | undefined,
108
+ export function getResolve(
115
109
  locale: Locale,
116
110
  messages: MessagesByLocale,
117
- overrides: Overrides | undefined,
118
- ): string {
119
- const result = lookup(key, locale, messages, overrides);
120
-
121
- if (result === null) {
122
- // Fires ONLY when a key is missing from every source including the
123
- // shipped `en` catalog — a real bug (typo, stale catalog, deleted key).
124
- // Fallback to `en` from a non-en locale is expected and stays silent,
125
- // matching the FormatJS / i18next default.
126
- warnOnce(
127
- `astryx-i18n:${locale}::${key}`,
128
- 'astryx-i18n',
129
- `missing key: ${key} (locale: ${locale})`,
130
- );
131
- return key;
132
- }
111
+ overrides?: Overrides,
112
+ ) {
113
+ const lookup = getLookup(locale, messages, overrides);
114
+
115
+ return (key: string, values: Record<string, unknown> | undefined) => {
116
+ const result = lookup[key] ?? EN_CATALOG[key]?.defaultMessage;
117
+ if (result === undefined) {
118
+ // Fires ONLY when a key is missing from every source including the
119
+ // shipped `en` catalog a real bug (typo, stale catalog, deleted key).
120
+ // Fallback to `en` from a non-en locale is expected and stays silent,
121
+ // matching the FormatJS / i18next default.
122
+ warnOnce(
123
+ `astryx-i18n:${locale}::${key}`,
124
+ 'astryx-i18n',
125
+ `missing key: ${key} (locale: ${locale})`,
126
+ );
127
+ return key;
128
+ }
133
129
 
134
- if (values === undefined) {
135
- // Static string — skip the parser entirely for the common case
136
- return result;
137
- }
130
+ if (values === undefined) {
131
+ // Static string — skip the parser entirely for the common case
132
+ return result;
133
+ }
138
134
 
139
- const formatted = getFormatter(result, locale).format(values);
140
- // IntlMessageFormat.format returns string | (string | React elements) — we
141
- // only ever pass string values so it will be a string; assert for the type
142
- // system.
143
- return formatted as string;
135
+ const formatted = getFormatter(result, locale).format(values);
136
+ // IntlMessageFormat.format returns string | (string | React elements) — we
137
+ // only ever pass string values so it will be a string; assert for the type
138
+ // system.
139
+ return formatted as string;
140
+ };
144
141
  }
145
142
 
146
143
  /**
@@ -19,9 +19,8 @@
19
19
  * - /packages/core/src/i18n/t.client.ts
20
20
  */
21
21
 
22
- import {use, useCallback} from 'react';
22
+ import {use} from 'react';
23
23
  import {InternationalizationContext} from './InternationalizationContext';
24
- import {resolve} from './resolve';
25
24
 
26
25
  export type TranslatorFn = (
27
26
  key: string,
@@ -42,9 +41,5 @@ export type TranslatorFn = (
42
41
  */
43
42
  export function useTranslator(): TranslatorFn {
44
43
  const ctx = use(InternationalizationContext);
45
- return useCallback(
46
- (key: string, values?: Record<string, unknown>) =>
47
- resolve(key, values, ctx.locale, ctx.messages, ctx.overrides),
48
- [ctx.locale, ctx.messages, ctx.overrides],
49
- );
44
+ return ctx.translate;
50
45
  }