@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.
- package/CHANGELOG.md +19 -0
- package/dist/i18n/InternationalizationContext.d.ts +2 -0
- package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
- package/dist/i18n/InternationalizationContext.js +3 -1
- package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
- package/dist/i18n/InternationalizationProvider.js +3 -1
- package/dist/i18n/resolve.d.ts +1 -1
- package/dist/i18n/resolve.d.ts.map +1 -1
- package/dist/i18n/resolve.js +37 -38
- package/dist/i18n/useTranslator.d.ts.map +1 -1
- package/dist/i18n/useTranslator.js +2 -3
- package/package.json +2 -2
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
- package/src/ComplexSelector/ComplexSelector.doc.mjs +2 -2
- package/src/DateInput/DateInput.doc.mjs +1 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
- package/src/Field/Field.doc.mjs +1 -1
- package/src/FieldStatus/FieldStatus.doc.mjs +1 -1
- package/src/FileInput/FileInput.doc.mjs +1 -1
- package/src/Indicator/Indicator.doc.mjs +1 -1
- package/src/InputGroup/InputGroup.doc.mjs +1 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +1 -1
- package/src/NumberInput/NumberInput.doc.mjs +1 -1
- package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
- package/src/RadioList/RadioList.doc.mjs +1 -1
- package/src/Selector/Selector.doc.mjs +1 -1
- package/src/Slider/Slider.doc.mjs +2 -2
- package/src/Switch/Switch.doc.mjs +1 -1
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +1 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
- package/src/Typeahead/Typeahead.doc.mjs +1 -1
- package/src/i18n/InternationalizationContext.ts +3 -0
- package/src/i18n/InternationalizationProvider.tsx +8 -1
- package/src/i18n/__tests__/resolve.test.ts +23 -1
- package/src/i18n/resolve.ts +46 -49
- 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;
|
|
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;
|
|
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,
|
package/dist/i18n/resolve.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|
package/dist/i18n/resolve.js
CHANGED
|
@@ -69,55 +69,54 @@ export function resolveLocaleChain(locale) {
|
|
|
69
69
|
}
|
|
70
70
|
return chain;
|
|
71
71
|
}
|
|
72
|
-
function
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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":"
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
672
|
+
"@astryxdesign/cli": "0.5.2"
|
|
673
673
|
},
|
|
674
674
|
"dependencies": {
|
|
675
675
|
"intl-messageformat": "^11.2.9"
|
|
@@ -6,7 +6,7 @@ export const docs = {
|
|
|
6
6
|
name: 'CheckboxList',
|
|
7
7
|
displayName: 'Checkbox List',
|
|
8
8
|
group: 'Checkbox',
|
|
9
|
-
category: '
|
|
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: '
|
|
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: '
|
|
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: {
|
package/src/Field/Field.doc.mjs
CHANGED
|
@@ -6,7 +6,7 @@ export const docs = {
|
|
|
6
6
|
name: 'Field',
|
|
7
7
|
displayName: 'Field',
|
|
8
8
|
group: 'Field',
|
|
9
|
-
category: '
|
|
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: '
|
|
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.',
|
|
@@ -6,7 +6,7 @@ export const docs = {
|
|
|
6
6
|
name: 'RadioList',
|
|
7
7
|
displayName: 'Radio List',
|
|
8
8
|
group: 'Radio',
|
|
9
|
-
category: '
|
|
9
|
+
category: 'Form Controls',
|
|
10
10
|
keywords: ["radiolist","radio","radiogroup","radiobutton","optionlist","singlechoice","choicelist"],
|
|
11
11
|
theming: {
|
|
12
12
|
targets: [
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export const docs = {
|
|
6
6
|
name: 'Slider',
|
|
7
7
|
displayName: 'Slider',
|
|
8
|
-
category: '
|
|
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
|
+
};
|
|
@@ -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
|
-
() => ({
|
|
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,
|
|
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', () => {
|
package/src/i18n/resolve.ts
CHANGED
|
@@ -73,74 +73,71 @@ export function resolveLocaleChain(locale: Locale): Locale[] {
|
|
|
73
73
|
return chain;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function
|
|
77
|
-
key: string,
|
|
76
|
+
function getLookup(
|
|
78
77
|
locale: Locale,
|
|
79
78
|
messages: MessagesByLocale,
|
|
80
|
-
overrides
|
|
81
|
-
): string
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
key: string,
|
|
114
|
-
values: Record<string, unknown> | undefined,
|
|
108
|
+
export function getResolve(
|
|
115
109
|
locale: Locale,
|
|
116
110
|
messages: MessagesByLocale,
|
|
117
|
-
overrides
|
|
118
|
-
)
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
if (values === undefined) {
|
|
131
|
+
// Static string — skip the parser entirely for the common case
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
138
134
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
}
|