Skip to content
This repository was archived by the owner on May 4, 2020. It is now read-only.

Commit ed752b7

Browse files
authored
feature(intl-messageformat): add core entry point (#105)
1 parent 43a48a0 commit ed752b7

File tree

6 files changed

+300
-241
lines changed

6 files changed

+300
-241
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
lib
22
tests/*.js*
33
!tests/tsconfig.json
4-
.tsbuildinfo
4+
.tsbuildinfo
5+
core.js

packages/intl-messageformat/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,23 @@ console.log(output); // => "The price is: $100.00"
176176

177177
In this example, we're defining a `USD` number format style which is passed to the underlying `Intl.NumberFormat` instance as its options.
178178

179+
## Advanced Usage
180+
181+
We also expose another entry point via `intl-messageformat/core` that does not contain the parser from `intl-messageformat-parser`. This is significantly smaller than the regular package but expects the message passed in to be in `AST` form instead of string. E.g:
182+
183+
```ts
184+
import IntlMessageFormat from 'intl-messageformat';
185+
new IntlMessageFormat('hello').format(); // prints out hello
186+
187+
// is equivalent to
188+
189+
import IntlMessageFormat from 'intl-messageformat/core';
190+
import parser from 'intl-messageformat-parser';
191+
new IntlMessageFormat(parser.parse('hello')).format(); // prints out hello
192+
```
193+
194+
This helps performance for cases like SSR or preload/precompilation-supported platforms since `AST` can be cached.
195+
179196
## Examples
180197

181198
### Plural Label

packages/intl-messageformat/rollup.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,14 @@ export default [
2727
},
2828
plugins: [resolveConfig, uglifyConfig]
2929
},
30+
{
31+
input: './lib/core.js',
32+
output: {
33+
sourcemap: false,
34+
file: 'core.js',
35+
format: 'cjs'
36+
},
37+
plugins: [resolveConfig]
38+
},
3039
...testRollupConfig
3140
];
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
Copyright (c) 2014, Yahoo! Inc. All rights reserved.
3+
Copyrights licensed under the New BSD License.
4+
See the accompanying LICENSE file for terms.
5+
*/
6+
7+
/* jslint esnext: true */
8+
9+
import Compiler, { Formats, isSelectOrPluralFormat, Pattern } from './compiler';
10+
import parser, { MessageFormatPattern } from 'intl-messageformat-parser';
11+
12+
// -- MessageFormat --------------------------------------------------------
13+
14+
function resolveLocale(locales: string | string[]): string {
15+
if (typeof locales === 'string') {
16+
locales = [locales];
17+
}
18+
try {
19+
return Intl.NumberFormat.supportedLocalesOf(locales, {
20+
// IE11 localeMatcher `lookup` seems to convert `en` -> `en-US`
21+
// but not other browsers,
22+
localeMatcher: 'best fit'
23+
})[0];
24+
} catch (e) {
25+
return MessageFormat.defaultLocale;
26+
}
27+
}
28+
29+
function formatPatterns(
30+
pattern: Pattern[],
31+
values?: Record<string, string | number | boolean | null | undefined>
32+
) {
33+
let result = '';
34+
for (const part of pattern) {
35+
// Exist early for string parts.
36+
if (typeof part === 'string') {
37+
result += part;
38+
continue;
39+
}
40+
41+
const { id } = part;
42+
43+
// Enforce that all required values are provided by the caller.
44+
if (!(values && id in values)) {
45+
throw new FormatError(`A value must be provided for: ${id}`, id);
46+
}
47+
48+
const value = values[id];
49+
50+
// Recursively format plural and select parts' option — which can be a
51+
// nested pattern structure. The choosing of the option to use is
52+
// abstracted-by and delegated-to the part helper object.
53+
if (isSelectOrPluralFormat(part)) {
54+
result += formatPatterns(part.getOption(value as any), values);
55+
} else {
56+
result += part.format(value as any);
57+
}
58+
}
59+
60+
return result;
61+
}
62+
63+
function mergeConfig(c1: Record<string, object>, c2?: Record<string, object>) {
64+
if (!c2) {
65+
return c1;
66+
}
67+
return {
68+
...(c1 || {}),
69+
...(c2 || {}),
70+
...Object.keys(c1).reduce((all: Record<string, object>, k) => {
71+
all[k] = {
72+
...c1[k],
73+
...(c2[k] || {})
74+
};
75+
return all;
76+
}, {})
77+
};
78+
}
79+
80+
function mergeConfigs(
81+
defaultConfig: Formats,
82+
configs?: Partial<Formats>
83+
): Formats {
84+
if (!configs) {
85+
return defaultConfig;
86+
}
87+
88+
return (Object.keys(defaultConfig) as Array<keyof Formats>).reduce(
89+
(all: Formats, k: keyof Formats) => {
90+
all[k] = mergeConfig(defaultConfig[k], configs[k]);
91+
return all;
92+
},
93+
{ ...defaultConfig }
94+
);
95+
}
96+
97+
class FormatError extends Error {
98+
public readonly variableId?: string;
99+
constructor(msg?: string, variableId?: string) {
100+
super(msg);
101+
this.variableId = variableId;
102+
}
103+
}
104+
105+
export interface IntlMessageFormat {
106+
new (
107+
message: string | MessageFormatPattern,
108+
locales?: string | string[],
109+
overrideFormats?: Partial<Formats>
110+
): IntlMessageFormat;
111+
(
112+
message: string | MessageFormatPattern,
113+
locales?: string | string[],
114+
overrideFormats?: Partial<Formats>
115+
): IntlMessageFormat;
116+
format(
117+
values?: Record<string, string | number | boolean | null | undefined>
118+
): string;
119+
resolvedOptions(): { locale: string };
120+
getAst(): ReturnType<typeof parser['parse']>;
121+
defaultLocale: string;
122+
formats: Formats;
123+
__parse: typeof parser['parse'];
124+
}
125+
126+
export const MessageFormat: IntlMessageFormat = ((
127+
message: string | MessageFormatPattern,
128+
locales: string | string[] = MessageFormat.defaultLocale,
129+
overrideFormats?: Partial<Formats>
130+
) => {
131+
// Parse string messages into an AST.
132+
const ast =
133+
typeof message === 'string' ? MessageFormat.__parse(message) : message;
134+
135+
if (!(ast && ast.type === 'messageFormatPattern')) {
136+
throw new TypeError('A message must be provided as a String or AST.');
137+
}
138+
139+
// Creates a new object with the specified `formats` merged with the default
140+
// formats.
141+
const formats = mergeConfigs(MessageFormat.formats, overrideFormats);
142+
143+
// Defined first because it's used to build the format pattern.
144+
const locale = resolveLocale(locales || []);
145+
146+
// Compile the `ast` to a pattern that is highly optimized for repeated
147+
// `format()` invocations. **Note:** This passes the `locales` set provided
148+
// to the constructor instead of just the resolved locale.
149+
const pattern = new Compiler(locales, formats).compile(ast);
150+
151+
// "Bind" `format()` method to `this` so it can be passed by reference like
152+
// the other `Intl` APIs.
153+
return {
154+
format(
155+
values?: Record<string, string | number | boolean | null | undefined>
156+
) {
157+
try {
158+
return formatPatterns(pattern, values);
159+
} catch (e) {
160+
if (e.variableId) {
161+
throw new Error(
162+
`The intl string context variable '${e.variableId}' was not provided to the string '${message}'`
163+
);
164+
} else {
165+
throw e;
166+
}
167+
}
168+
},
169+
resolvedOptions() {
170+
return { locale };
171+
},
172+
getAst() {
173+
return ast;
174+
}
175+
};
176+
}) as any;
177+
178+
MessageFormat.defaultLocale = 'en';
179+
// Default format options used as the prototype of the `formats` provided to the
180+
// constructor. These are used when constructing the internal Intl.NumberFormat
181+
// and Intl.DateTimeFormat instances.
182+
MessageFormat.formats = {
183+
number: {
184+
currency: {
185+
style: 'currency'
186+
},
187+
188+
percent: {
189+
style: 'percent'
190+
}
191+
},
192+
193+
date: {
194+
short: {
195+
month: 'numeric',
196+
day: 'numeric',
197+
year: '2-digit'
198+
},
199+
200+
medium: {
201+
month: 'short',
202+
day: 'numeric',
203+
year: 'numeric'
204+
},
205+
206+
long: {
207+
month: 'long',
208+
day: 'numeric',
209+
year: 'numeric'
210+
},
211+
212+
full: {
213+
weekday: 'long',
214+
month: 'long',
215+
day: 'numeric',
216+
year: 'numeric'
217+
}
218+
},
219+
220+
time: {
221+
short: {
222+
hour: 'numeric',
223+
minute: 'numeric'
224+
},
225+
226+
medium: {
227+
hour: 'numeric',
228+
minute: 'numeric',
229+
second: 'numeric'
230+
},
231+
232+
long: {
233+
hour: 'numeric',
234+
minute: 'numeric',
235+
second: 'numeric',
236+
timeZoneName: 'short'
237+
},
238+
239+
full: {
240+
hour: 'numeric',
241+
minute: 'numeric',
242+
second: 'numeric',
243+
timeZoneName: 'short'
244+
}
245+
}
246+
};
247+
248+
export { Formats, Pattern } from './compiler';
249+
export default MessageFormat;

0 commit comments

Comments
 (0)