@messagevisor/react 0.0.1 → 0.2.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 +24 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/jest.config.js +13 -0
- package/lib/MessagevisorContext.d.ts +21 -0
- package/lib/MessagevisorContext.js +39 -0
- package/lib/MessagevisorContext.js.map +1 -0
- package/lib/MessagevisorProvider.d.ts +12 -0
- package/lib/MessagevisorProvider.js +58 -0
- package/lib/MessagevisorProvider.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +23 -0
- package/lib/index.js.map +1 -0
- package/lib/useMessagevisor.d.ts +20 -0
- package/lib/useMessagevisor.js +106 -0
- package/lib/useMessagevisor.js.map +1 -0
- package/lib/useMessagevisorSnapshot.d.ts +2 -0
- package/lib/useMessagevisorSnapshot.js +77 -0
- package/lib/useMessagevisorSnapshot.js.map +1 -0
- package/lib/useReactiveMessagevisor.d.ts +31 -0
- package/lib/useReactiveMessagevisor.js +141 -0
- package/lib/useReactiveMessagevisor.js.map +1 -0
- package/lib/useRichText.d.ts +13 -0
- package/lib/useRichText.js +112 -0
- package/lib/useRichText.js.map +1 -0
- package/lib/useSdk.d.ts +2 -0
- package/lib/useSdk.js +46 -0
- package/lib/useSdk.js.map +1 -0
- package/package.json +51 -13
- package/src/MessagevisorContext.ts +28 -0
- package/src/MessagevisorProvider.spec.tsx +29 -0
- package/src/MessagevisorProvider.tsx +41 -0
- package/src/index.ts +6 -0
- package/src/testUtils.ts +72 -0
- package/src/useMessagevisor.spec.tsx +425 -0
- package/src/useMessagevisor.ts +115 -0
- package/src/useMessagevisorSnapshot.ts +65 -0
- package/src/useReactiveMessagevisor.spec.tsx +507 -0
- package/src/useReactiveMessagevisor.ts +223 -0
- package/src/useRichText.tsx +116 -0
- package/src/useSdk.spec.tsx +45 -0
- package/src/useSdk.ts +15 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.typecheck.json +4 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
EvaluationOptions,
|
|
5
|
+
TranslateOptions,
|
|
6
|
+
MessagePrimitiveValue,
|
|
7
|
+
Messagevisor,
|
|
8
|
+
} from "@messagevisor/sdk";
|
|
9
|
+
import type {
|
|
10
|
+
Context,
|
|
11
|
+
FormatDateTimePresetOptions,
|
|
12
|
+
FormatNumberPresetOptions,
|
|
13
|
+
FormatRelativeTimePresetOptions,
|
|
14
|
+
LocaleDirection,
|
|
15
|
+
LocaleKey,
|
|
16
|
+
MessageKey,
|
|
17
|
+
} from "@messagevisor/types";
|
|
18
|
+
|
|
19
|
+
import { useMessagevisorSnapshot } from "./useMessagevisorSnapshot";
|
|
20
|
+
import { useRichText, type ReactMessageValues, type ReactRichMessageValues } from "./useRichText";
|
|
21
|
+
import { useSdk } from "./useSdk";
|
|
22
|
+
|
|
23
|
+
export interface LocaleInfo {
|
|
24
|
+
locale: LocaleKey | null;
|
|
25
|
+
direction: LocaleDirection | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function useReactiveSdk() {
|
|
29
|
+
const sdk = useSdk();
|
|
30
|
+
useMessagevisorSnapshot();
|
|
31
|
+
|
|
32
|
+
return sdk;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useLocale(): LocaleKey | null {
|
|
36
|
+
return useMessagevisorSnapshot().locale;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useDirection(): LocaleDirection | undefined {
|
|
40
|
+
return useMessagevisorSnapshot().direction;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useLocaleInfo(): LocaleInfo {
|
|
44
|
+
const snapshot = useMessagevisorSnapshot();
|
|
45
|
+
|
|
46
|
+
return React.useMemo(
|
|
47
|
+
() => ({
|
|
48
|
+
locale: snapshot.locale,
|
|
49
|
+
direction: snapshot.direction,
|
|
50
|
+
}),
|
|
51
|
+
[snapshot.locale, snapshot.direction],
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useMessagevisorContext(): Context {
|
|
56
|
+
return useMessagevisorSnapshot().context;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useCurrency(): string | undefined {
|
|
60
|
+
return useMessagevisorSnapshot().currency;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useTimeZone(): string | undefined {
|
|
64
|
+
return useMessagevisorSnapshot().timeZone;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useTranslation(
|
|
68
|
+
messageKey: MessageKey,
|
|
69
|
+
values?: Record<string, MessagePrimitiveValue>,
|
|
70
|
+
options?: TranslateOptions,
|
|
71
|
+
): string;
|
|
72
|
+
export function useTranslation(
|
|
73
|
+
messageKey: MessageKey,
|
|
74
|
+
values: ReactRichMessageValues,
|
|
75
|
+
options?: TranslateOptions,
|
|
76
|
+
): React.ReactNode;
|
|
77
|
+
export function useTranslation(
|
|
78
|
+
messageKey: MessageKey,
|
|
79
|
+
values?: ReactMessageValues,
|
|
80
|
+
options?: TranslateOptions,
|
|
81
|
+
): string | React.ReactNode {
|
|
82
|
+
const sdk = useReactiveSdk();
|
|
83
|
+
const richText = useRichText();
|
|
84
|
+
const message = sdk.getRawTranslation(messageKey, options);
|
|
85
|
+
const translation = sdk.translate<React.ReactNode>(
|
|
86
|
+
messageKey,
|
|
87
|
+
richText.mergeValues(values, message),
|
|
88
|
+
options,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return richText.wrapResult(
|
|
92
|
+
richText.runModules(translation, {
|
|
93
|
+
source: "translation",
|
|
94
|
+
messageKey,
|
|
95
|
+
}),
|
|
96
|
+
) as React.ReactNode;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function useFormatMessage(
|
|
100
|
+
message: string,
|
|
101
|
+
values?: Record<string, MessagePrimitiveValue>,
|
|
102
|
+
options?: EvaluationOptions,
|
|
103
|
+
): string;
|
|
104
|
+
export function useFormatMessage(
|
|
105
|
+
message: string,
|
|
106
|
+
values: ReactRichMessageValues,
|
|
107
|
+
options?: EvaluationOptions,
|
|
108
|
+
): React.ReactNode;
|
|
109
|
+
export function useFormatMessage(
|
|
110
|
+
message: string,
|
|
111
|
+
values?: ReactMessageValues,
|
|
112
|
+
options?: EvaluationOptions,
|
|
113
|
+
): string | React.ReactNode {
|
|
114
|
+
const sdk = useReactiveSdk();
|
|
115
|
+
const richText = useRichText();
|
|
116
|
+
const translation = sdk.formatMessage(message, richText.mergeValues(values, message), options);
|
|
117
|
+
|
|
118
|
+
return richText.wrapResult(
|
|
119
|
+
richText.runModules(translation, {
|
|
120
|
+
source: "formatMessage",
|
|
121
|
+
}),
|
|
122
|
+
) as React.ReactNode;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useFormatNumber(
|
|
126
|
+
value: number,
|
|
127
|
+
presetOrOptions?: string | FormatNumberPresetOptions,
|
|
128
|
+
options?: EvaluationOptions,
|
|
129
|
+
) {
|
|
130
|
+
return useReactiveSdk().formatNumber(value, presetOrOptions, options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function useFormatNumberToParts(
|
|
134
|
+
value: number,
|
|
135
|
+
presetOrOptions?: string | FormatNumberPresetOptions,
|
|
136
|
+
options?: EvaluationOptions,
|
|
137
|
+
) {
|
|
138
|
+
return useReactiveSdk().formatNumberToParts(value, presetOrOptions, options);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function useFormatDate(
|
|
142
|
+
value: Date | number | string,
|
|
143
|
+
presetOrOptions?: string | FormatDateTimePresetOptions,
|
|
144
|
+
options?: EvaluationOptions,
|
|
145
|
+
) {
|
|
146
|
+
return useReactiveSdk().formatDate(value, presetOrOptions, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function useFormatDateToParts(
|
|
150
|
+
value: Date | number | string,
|
|
151
|
+
presetOrOptions?: string | FormatDateTimePresetOptions,
|
|
152
|
+
options?: EvaluationOptions,
|
|
153
|
+
) {
|
|
154
|
+
return useReactiveSdk().formatDateToParts(value, presetOrOptions, options);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function useFormatTime(
|
|
158
|
+
value: Date | number | string,
|
|
159
|
+
presetOrOptions?: string | FormatDateTimePresetOptions,
|
|
160
|
+
options?: EvaluationOptions,
|
|
161
|
+
) {
|
|
162
|
+
return useReactiveSdk().formatTime(value, presetOrOptions, options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function useFormatTimeToParts(
|
|
166
|
+
value: Date | number | string,
|
|
167
|
+
presetOrOptions?: string | FormatDateTimePresetOptions,
|
|
168
|
+
options?: EvaluationOptions,
|
|
169
|
+
) {
|
|
170
|
+
return useReactiveSdk().formatTimeToParts(value, presetOrOptions, options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useFormatDateTimeRange(
|
|
174
|
+
start: Date | number | string,
|
|
175
|
+
end: Date | number | string,
|
|
176
|
+
presetOrOptions?: string | FormatDateTimePresetOptions,
|
|
177
|
+
options?: EvaluationOptions,
|
|
178
|
+
) {
|
|
179
|
+
return useReactiveSdk().formatDateTimeRange(start, end, presetOrOptions, options);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function useFormatRelativeTime(
|
|
183
|
+
value: number,
|
|
184
|
+
unit: Intl.RelativeTimeFormatUnit,
|
|
185
|
+
presetOrOptions?: string | FormatRelativeTimePresetOptions,
|
|
186
|
+
options?: EvaluationOptions,
|
|
187
|
+
) {
|
|
188
|
+
return useReactiveSdk().formatRelativeTime(value, unit, presetOrOptions, options);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function useFormatPlural(value: number, options?: Intl.PluralRulesOptions) {
|
|
192
|
+
return useReactiveSdk().formatPlural(value, options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function useFormatList(values: string[], options?: any) {
|
|
196
|
+
return useReactiveSdk().formatList(values, options);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function useFormatListToParts(values: string[], options?: any) {
|
|
200
|
+
return useReactiveSdk().formatListToParts(values, options);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function useFormatDisplayName(value: string, options?: any) {
|
|
204
|
+
return useReactiveSdk().formatDisplayName(value, options);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type ReactiveMessagevisorApi = Pick<
|
|
208
|
+
Messagevisor,
|
|
209
|
+
| "translate"
|
|
210
|
+
| "formatMessage"
|
|
211
|
+
| "formatNumber"
|
|
212
|
+
| "formatNumberToParts"
|
|
213
|
+
| "formatDate"
|
|
214
|
+
| "formatDateToParts"
|
|
215
|
+
| "formatTime"
|
|
216
|
+
| "formatTimeToParts"
|
|
217
|
+
| "formatDateTimeRange"
|
|
218
|
+
| "formatRelativeTime"
|
|
219
|
+
| "formatPlural"
|
|
220
|
+
| "formatList"
|
|
221
|
+
| "formatListToParts"
|
|
222
|
+
| "formatDisplayName"
|
|
223
|
+
>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
MessagePrimitiveValue,
|
|
5
|
+
MessageValue,
|
|
6
|
+
MessagevisorTranslationSource,
|
|
7
|
+
} from "@messagevisor/sdk";
|
|
8
|
+
import type { MessageKey } from "@messagevisor/types";
|
|
9
|
+
|
|
10
|
+
import { MessagevisorContext } from "./MessagevisorContext";
|
|
11
|
+
|
|
12
|
+
export type ReactMessageValues = Record<string, MessageValue<React.ReactNode>>;
|
|
13
|
+
export type ReactRichMessageValues = Record<
|
|
14
|
+
string,
|
|
15
|
+
MessagePrimitiveValue | ((chunks: React.ReactNode[]) => React.ReactNode)
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
const RICH_TAG_NAME_PATTERN = /<([A-Za-z][A-Za-z0-9_-]*)\b[^>]*>/g;
|
|
19
|
+
|
|
20
|
+
function getRichTagNames(message: string) {
|
|
21
|
+
const tags = new Set<string>();
|
|
22
|
+
let match: RegExpExecArray | null;
|
|
23
|
+
|
|
24
|
+
while ((match = RICH_TAG_NAME_PATTERN.exec(message))) {
|
|
25
|
+
tags.add(match[1]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return tags;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useRichText() {
|
|
32
|
+
const context = React.useContext(MessagevisorContext);
|
|
33
|
+
|
|
34
|
+
if (!context) {
|
|
35
|
+
throw new Error("useSdk must be used within MessagevisorProvider.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return React.useMemo(() => {
|
|
39
|
+
function mergeValues(values?: ReactMessageValues, message?: string) {
|
|
40
|
+
const defaults = context.defaultRichTextElements;
|
|
41
|
+
const defaultKeys = Object.keys(defaults);
|
|
42
|
+
|
|
43
|
+
if (!message || defaultKeys.length === 0) {
|
|
44
|
+
return values;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tagNames = getRichTagNames(message);
|
|
48
|
+
const matchingDefaults = Object.fromEntries(
|
|
49
|
+
defaultKeys.filter((key) => tagNames.has(key)).map((key) => [key, defaults[key]]),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (Object.keys(matchingDefaults).length === 0) {
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...matchingDefaults,
|
|
58
|
+
...(values || {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function wrapResult<T>(result: T) {
|
|
63
|
+
if (!context.wrapRichTextChunksInFragment || !Array.isArray(result)) {
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<React.Fragment>
|
|
69
|
+
{result.map((chunk, index) => (
|
|
70
|
+
<React.Fragment key={index}>{chunk}</React.Fragment>
|
|
71
|
+
))}
|
|
72
|
+
</React.Fragment>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runModules<TTranslation>(
|
|
77
|
+
translation: TTranslation,
|
|
78
|
+
payload: {
|
|
79
|
+
source: MessagevisorTranslationSource;
|
|
80
|
+
messageKey?: MessageKey;
|
|
81
|
+
},
|
|
82
|
+
) {
|
|
83
|
+
let currentTranslation = translation as React.ReactNode;
|
|
84
|
+
const locale = context.instance.getLocale();
|
|
85
|
+
|
|
86
|
+
if (!locale) {
|
|
87
|
+
return translation;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const module of context.modules) {
|
|
91
|
+
const nextTranslation = module.transform?.({
|
|
92
|
+
translation: currentTranslation,
|
|
93
|
+
locale,
|
|
94
|
+
...payload,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (typeof nextTranslation !== "undefined") {
|
|
98
|
+
currentTranslation = nextTranslation;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return currentTranslation;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
mergeValues,
|
|
107
|
+
wrapResult,
|
|
108
|
+
runModules,
|
|
109
|
+
};
|
|
110
|
+
}, [
|
|
111
|
+
context.modules,
|
|
112
|
+
context.instance,
|
|
113
|
+
context.defaultRichTextElements,
|
|
114
|
+
context.wrapRichTextChunksInFragment,
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
|
|
5
|
+
import { MessagevisorProvider } from "./MessagevisorProvider";
|
|
6
|
+
import { createTestInstance } from "./testUtils";
|
|
7
|
+
import { useSdk } from "./useSdk";
|
|
8
|
+
|
|
9
|
+
describe("useSdk", function () {
|
|
10
|
+
it("returns the provided SDK instance", function () {
|
|
11
|
+
const instance = createTestInstance();
|
|
12
|
+
const seen: unknown[] = [];
|
|
13
|
+
|
|
14
|
+
function TestComponent() {
|
|
15
|
+
seen.push(useSdk());
|
|
16
|
+
|
|
17
|
+
return <p>ok</p>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { rerender } = render(
|
|
21
|
+
<MessagevisorProvider instance={instance}>
|
|
22
|
+
<TestComponent />
|
|
23
|
+
</MessagevisorProvider>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
rerender(
|
|
27
|
+
<MessagevisorProvider instance={instance}>
|
|
28
|
+
<TestComponent />
|
|
29
|
+
</MessagevisorProvider>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(screen.getByText("ok")).toBeInTheDocument();
|
|
33
|
+
expect(seen).toEqual([instance, instance]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("throws a clear error outside MessagevisorProvider", function () {
|
|
37
|
+
function Orphan() {
|
|
38
|
+
useSdk();
|
|
39
|
+
|
|
40
|
+
return <p>orphan</p>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(() => render(<Orphan />)).toThrow("useSdk must be used within MessagevisorProvider.");
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/useSdk.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import type { Messagevisor } from "@messagevisor/sdk";
|
|
4
|
+
|
|
5
|
+
import { MessagevisorContext } from "./MessagevisorContext";
|
|
6
|
+
|
|
7
|
+
export function useSdk(): Messagevisor {
|
|
8
|
+
const context = React.useContext(MessagevisorContext);
|
|
9
|
+
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error("useSdk must be used within MessagevisorProvider.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return context.instance;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.cjs.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./lib",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"esModuleInterop": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
|
10
|
+
"exclude": ["./src/**/*.spec.ts", "./src/**/*.spec.tsx", "./src/testUtils.ts"]
|
|
11
|
+
}
|