@discord/intl 0.0.5 → 0.0.6-rc.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.
@@ -1,5 +1,5 @@
1
1
  import { IntlShape } from '@formatjs/intl';
2
- import { LocaleImportMap, MessageLoader } from './message-loader';
2
+ import { LocaleImportMap, LocaleId, MessageLoader } from './message-loader';
3
3
  import type { TypedIntlMessageGetter, FormatValuesFor, RichTextElementMap } from './types';
4
4
  /**
5
5
  * Fallback locale used for all internationalization when an operation in the
@@ -56,4 +56,4 @@ export declare class IntlManager<DefaultElements extends RichTextElementMap, Def
56
56
  * different locales and sanity checks as needed to provide accessors for each
57
57
  * message defined in `messageKeys`.
58
58
  */
59
- export declare function createLoader(messageKeys: string[], localeImportMap: LocaleImportMap): MessageLoader;
59
+ export declare function createLoader(messageKeys: string[], localeImportMap: LocaleImportMap, defaultLocale: LocaleId): MessageLoader;
@@ -133,6 +133,6 @@ exports.IntlManager = IntlManager;
133
133
  * different locales and sanity checks as needed to provide accessors for each
134
134
  * message defined in `messageKeys`.
135
135
  */
136
- function createLoader(messageKeys, localeImportMap) {
137
- return new message_loader_1.MessageLoader(messageKeys, localeImportMap);
136
+ function createLoader(messageKeys, localeImportMap, defaultLocale) {
137
+ return new message_loader_1.MessageLoader(messageKeys, localeImportMap, defaultLocale);
138
138
  }
@@ -8,7 +8,7 @@ export interface IntlMessageGetterAdditions {
8
8
  export interface IntlMessageGetter extends IntlMessageGetterAdditions {
9
9
  (locale: LocaleId): InternalIntlMessage;
10
10
  }
11
- type LocaleId = string;
11
+ export type LocaleId = string;
12
12
  export type LocaleImportMap = Record<LocaleId, () => Promise<{
13
13
  default: MessagesData;
14
14
  }>>;
@@ -18,6 +18,12 @@ export declare class MessageLoader {
18
18
  messages: Record<LocaleId, MessagesData>;
19
19
  localeImportMap: LocaleImportMap;
20
20
  supportedLocales: LocaleId[];
21
+ /**
22
+ * Fallback locale where messages will try to be looked up if they don't exist in the requested
23
+ * locale in a given call. This locale will also be immediately loaded when this loader is
24
+ * constructed to ensure the contents are available as soon as possible.
25
+ */
26
+ defaultLocale: LocaleId;
21
27
  /**
22
28
  * Promise representing the current locale being loaded. If this is non-null,
23
29
  * then a request has already been made to load the locale and it is waiting
@@ -35,9 +41,48 @@ export declare class MessageLoader {
35
41
  * when it has loaded.
36
42
  */
37
43
  _subscribers: Set<() => void>;
44
+ /**
45
+ * Message to show as a fallback when the requested message is unavailable.
46
+ */
38
47
  fallbackMessage: InternalIntlMessage;
39
- constructor(messageKeys: string[], localeImportMap: LocaleImportMap);
48
+ /**
49
+ * Map from hashed message keys to their original values, to provide context in error messages
50
+ * that would otherwise be obfuscated.
51
+ *
52
+ * Consumers should only provide this value in developer or debug environments.
53
+ *
54
+ * @private
55
+ */
56
+ _debugKeyMap: Record<string, string>;
57
+ /**
58
+ * Map of locale names to source file names, used to provide context for where messages are
59
+ * imported from during development.
60
+ *
61
+ * Consumers should only provide this value in developer or debug environments.
62
+ *
63
+ * @private
64
+ */
65
+ _localeFileMap: Record<string, string>;
66
+ constructor(messageKeys: string[], localeImportMap: LocaleImportMap, defaultLocale: LocaleId);
67
+ /**
68
+ * Provide additional debug information to use during development, providing additional context
69
+ * for console error messages.
70
+ *
71
+ * @param {Record<string, string>} keyMap
72
+ * @param {Record<string, string>} localeFileMap
73
+ */
74
+ withDebugValues(keyMap: Record<string, string>, localeFileMap: Record<string, string>): void;
40
75
  get(key: string, locale: LocaleId): InternalIntlMessage;
76
+ /**
77
+ * Return the value of the message with the given `key` in the given `locale`. If the message has
78
+ * no value in that locale, this function returns `undefined`.
79
+ *
80
+ * This function will first check the cache to see if it has already been loaded, and will later
81
+ * set the cache after parsing if not. If the requested locale has not yet been loaded, this
82
+ * function will trigger a load for that locale, but will not wait for it to resolve before
83
+ * returning.
84
+ */
85
+ getMessageValue(key: string, locale: LocaleId): InternalIntlMessage | undefined;
41
86
  /**
42
87
  * Returns a record mapping the keys this object manages to bound functions
43
88
  * for `get` with the that key as the first argument, allowing consumers to
@@ -46,42 +46,127 @@ var __values = (this && this.__values) || function(o) {
46
46
  };
47
47
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
48
48
  };
49
+ var __read = (this && this.__read) || function (o, n) {
50
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
51
+ if (!m) return o;
52
+ var i = m.call(o), r, ar = [], e;
53
+ try {
54
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
55
+ }
56
+ catch (error) { e = { error: error }; }
57
+ finally {
58
+ try {
59
+ if (r && !r.done && (m = i["return"])) m.call(i);
60
+ }
61
+ finally { if (e) throw e.error; }
62
+ }
63
+ return ar;
64
+ };
49
65
  Object.defineProperty(exports, "__esModule", { value: true });
50
66
  exports.MessageLoader = void 0;
51
67
  var message_1 = require("./message");
52
68
  var MessageLoader = /** @class */ (function () {
53
- function MessageLoader(messageKeys, localeImportMap) {
69
+ function MessageLoader(messageKeys, localeImportMap, defaultLocale) {
70
+ var e_1, _a;
71
+ var _this = this;
54
72
  this.messageKeys = messageKeys;
55
73
  this.messages = {};
56
74
  this.localeImportMap = localeImportMap;
57
75
  this.supportedLocales = Object.keys(localeImportMap);
76
+ this.defaultLocale = defaultLocale;
58
77
  this._localeLoadingPromises = {};
59
78
  this._parseCache = new Map();
60
79
  this._subscribers = new Set();
80
+ this._loadLocale(this.defaultLocale);
61
81
  this.fallbackMessage = new message_1.InternalIntlMessage('THIS MESSAGE FAILED TO LOAD', 'en-US');
82
+ // In cases where hot module replacement is available, set up cache clearing whenever the
83
+ // targets change so that values are always replaced.
84
+ // @ts-expect-error `hot` not defined in types.
85
+ if (module.hot) {
86
+ var _loop_1 = function (locale, file) {
87
+ // @ts-expect-error `hot` not defined in types.
88
+ module.hot.accept(file, function () {
89
+ _this._parseCache.clear();
90
+ _this._loadLocale(locale);
91
+ });
92
+ };
93
+ try {
94
+ for (var _b = __values(Object.entries(localeImportMap)), _c = _b.next(); !_c.done; _c = _b.next()) {
95
+ var _d = __read(_c.value, 2), locale = _d[0], file = _d[1];
96
+ _loop_1(locale, file);
97
+ }
98
+ }
99
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
100
+ finally {
101
+ try {
102
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
103
+ }
104
+ finally { if (e_1) throw e_1.error; }
105
+ }
106
+ }
62
107
  }
108
+ /**
109
+ * Provide additional debug information to use during development, providing additional context
110
+ * for console error messages.
111
+ *
112
+ * @param {Record<string, string>} keyMap
113
+ * @param {Record<string, string>} localeFileMap
114
+ */
115
+ MessageLoader.prototype.withDebugValues = function (keyMap, localeFileMap) {
116
+ this._debugKeyMap = keyMap;
117
+ this._localeFileMap = localeFileMap;
118
+ };
63
119
  MessageLoader.prototype.get = function (key, locale) {
120
+ var _a;
121
+ var value = (_a = this.getMessageValue(key, locale)) !== null && _a !== void 0 ? _a : this.getMessageValue(key, this.defaultLocale);
122
+ if (value != null) {
123
+ return value;
124
+ }
125
+ // If the message couldn't be found in either the requested nor the default locale, then
126
+ // nothing can be done.
127
+ var errorKey = this._debugKeyMap != null ? "\"".concat(this._debugKeyMap[key], "\" (").concat(key, ")") : undefined;
128
+ var requestedLocale = this._localeFileMap != null ? "".concat(locale, " (").concat(this._localeFileMap[locale], ")") : locale;
129
+ var defaultLocale = this._localeFileMap != null
130
+ ? "".concat(this.defaultLocale, " (").concat(this._localeFileMap[this.defaultLocale], ")")
131
+ : this.defaultLocale;
132
+ console.warn("Requested message ".concat(errorKey, " does not have a value in the requested locale ").concat(requestedLocale, " nor the default locale ").concat(defaultLocale));
133
+ return this.fallbackMessage;
134
+ };
135
+ /**
136
+ * Return the value of the message with the given `key` in the given `locale`. If the message has
137
+ * no value in that locale, this function returns `undefined`.
138
+ *
139
+ * This function will first check the cache to see if it has already been loaded, and will later
140
+ * set the cache after parsing if not. If the requested locale has not yet been loaded, this
141
+ * function will trigger a load for that locale, but will not wait for it to resolve before
142
+ * returning.
143
+ */
144
+ MessageLoader.prototype.getMessageValue = function (key, locale) {
64
145
  var cacheKey = key + '@' + locale;
65
146
  var cachedValue = this._parseCache.get(cacheKey);
66
147
  if (cachedValue != null)
67
148
  return cachedValue;
149
+ // Return early if this locale is still in the process of being loaded.
150
+ if (this._localeLoadingPromises[locale] != null) {
151
+ return undefined;
152
+ }
153
+ // If not, check whether it's been loaded and trigger a load if not.
68
154
  if (this.messages[locale] == null) {
69
155
  // Ensure the locale is loaded, if it is supported
70
156
  if (this.supportedLocales.includes(locale)) {
71
157
  this._loadLocale(locale);
72
158
  }
73
- // Otherwise, fallback to just the key value in the meantime.
74
- return this.fallbackMessage;
159
+ return undefined;
75
160
  }
76
- // Then try to return the loaded message
161
+ // Then try to return the loaded message.
77
162
  if (key in this.messages[locale]) {
78
163
  var content = this.messages[locale][key];
79
164
  var message = new message_1.InternalIntlMessage(content, locale);
80
165
  this._parseCache.set(cacheKey, message);
81
166
  return message;
82
167
  }
83
- // And finally throw if there's no match whatsoever
84
- throw new Error("Requested message ".concat(key, " does not have a value in the requested locale nor the default locale"));
168
+ // Otherwise just assume it doesn't exist.
169
+ return undefined;
85
170
  };
86
171
  /**
87
172
  * Returns a record mapping the keys this object manages to bound functions
@@ -92,7 +177,7 @@ var MessageLoader = /** @class */ (function () {
92
177
  MessageLoader.prototype.getBinds = function () {
93
178
  var _this = this;
94
179
  var onChange = this.onChange.bind(this);
95
- return this.messageKeys.reduce(function (acc, key) {
180
+ return Object.keys(this.messageKeys).reduce(function (acc, key) {
96
181
  var bound = _this.get.bind(_this, key);
97
182
  bound.onChange = onChange;
98
183
  acc[key] = bound;
@@ -101,7 +186,7 @@ var MessageLoader = /** @class */ (function () {
101
186
  };
102
187
  MessageLoader.prototype._loadLocale = function (locale) {
103
188
  return __awaiter(this, void 0, void 0, function () {
104
- var loadStart, loadingPromise, _a, _b, loadEnd;
189
+ var loadingPromise, _a, _b;
105
190
  return __generator(this, function (_c) {
106
191
  switch (_c.label) {
107
192
  case 0:
@@ -118,7 +203,6 @@ var MessageLoader = /** @class */ (function () {
118
203
  // If the locale is already set in `messages`, then it doesn't need to be loaded again.
119
204
  if (this.messages[locale] != null)
120
205
  return [2 /*return*/];
121
- loadStart = performance.now();
122
206
  loadingPromise = this.localeImportMap[locale]();
123
207
  this._localeLoadingPromises[locale] = loadingPromise;
124
208
  _a = this.messages;
@@ -127,7 +211,6 @@ var MessageLoader = /** @class */ (function () {
127
211
  case 1:
128
212
  _a[_b] = (_c.sent()).default;
129
213
  delete this._localeLoadingPromises[locale];
130
- loadEnd = performance.now();
131
214
  this.emitChange();
132
215
  return [2 /*return*/];
133
216
  }
@@ -139,19 +222,19 @@ var MessageLoader = /** @class */ (function () {
139
222
  * potentially update to get new values for messages.
140
223
  */
141
224
  MessageLoader.prototype.emitChange = function () {
142
- var e_1, _a;
225
+ var e_2, _a;
143
226
  try {
144
227
  for (var _b = __values(this._subscribers.values()), _c = _b.next(); !_c.done; _c = _b.next()) {
145
228
  var callback = _c.value;
146
229
  callback();
147
230
  }
148
231
  }
149
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
232
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
150
233
  finally {
151
234
  try {
152
235
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
153
236
  }
154
- finally { if (e_1) throw e_1.error; }
237
+ finally { if (e_2) throw e_2.error; }
155
238
  }
156
239
  };
157
240
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discord/intl",
3
- "version": "0.0.5",
3
+ "version": "0.0.6-rc.0",
4
4
  "license": "MIT",
5
5
  "description": "Client runtime for managing messages and translations in a React project.",
6
6
  "main": "./dist/index.js",
@@ -6,7 +6,7 @@ import {
6
6
  IntlMessageFormat,
7
7
  } from 'intl-messageformat';
8
8
 
9
- import { LocaleImportMap, MessageLoader } from './message-loader';
9
+ import { LocaleImportMap, LocaleId, MessageLoader } from './message-loader';
10
10
 
11
11
  import type { TypedIntlMessageGetter, FormatValuesFor, RichTextElementMap } from './types';
12
12
 
@@ -161,6 +161,10 @@ export class IntlManager<
161
161
  * different locales and sanity checks as needed to provide accessors for each
162
162
  * message defined in `messageKeys`.
163
163
  */
164
- export function createLoader(messageKeys: string[], localeImportMap: LocaleImportMap) {
165
- return new MessageLoader(messageKeys, localeImportMap);
164
+ export function createLoader(
165
+ messageKeys: string[],
166
+ localeImportMap: LocaleImportMap,
167
+ defaultLocale: LocaleId,
168
+ ) {
169
+ return new MessageLoader(messageKeys, localeImportMap, defaultLocale);
166
170
  }
@@ -12,7 +12,7 @@ export interface IntlMessageGetter extends IntlMessageGetterAdditions {
12
12
  (locale: LocaleId): InternalIntlMessage;
13
13
  }
14
14
 
15
- type LocaleId = string;
15
+ export type LocaleId = string;
16
16
 
17
17
  export type LocaleImportMap = Record<LocaleId, () => Promise<{ default: MessagesData }>>;
18
18
 
@@ -22,6 +22,12 @@ export class MessageLoader {
22
22
  messages: Record<LocaleId, MessagesData>;
23
23
  localeImportMap: LocaleImportMap;
24
24
  supportedLocales: LocaleId[];
25
+ /**
26
+ * Fallback locale where messages will try to be looked up if they don't exist in the requested
27
+ * locale in a given call. This locale will also be immediately loaded when this loader is
28
+ * constructed to ensure the contents are available as soon as possible.
29
+ */
30
+ defaultLocale: LocaleId;
25
31
 
26
32
  /**
27
33
  * Promise representing the current locale being loaded. If this is non-null,
@@ -41,37 +47,126 @@ export class MessageLoader {
41
47
  */
42
48
  _subscribers: Set<() => void>;
43
49
 
50
+ /**
51
+ * Message to show as a fallback when the requested message is unavailable.
52
+ */
44
53
  fallbackMessage: InternalIntlMessage;
45
54
 
46
- constructor(messageKeys: string[], localeImportMap: LocaleImportMap) {
55
+ ///
56
+ // Debug mode values
57
+ ///
58
+
59
+ /**
60
+ * Map from hashed message keys to their original values, to provide context in error messages
61
+ * that would otherwise be obfuscated.
62
+ *
63
+ * Consumers should only provide this value in developer or debug environments.
64
+ *
65
+ * @private
66
+ */
67
+ _debugKeyMap: Record<string, string>;
68
+ /**
69
+ * Map of locale names to source file names, used to provide context for where messages are
70
+ * imported from during development.
71
+ *
72
+ * Consumers should only provide this value in developer or debug environments.
73
+ *
74
+ * @private
75
+ */
76
+ _localeFileMap: Record<string, string>;
77
+
78
+ constructor(messageKeys: string[], localeImportMap: LocaleImportMap, defaultLocale: LocaleId) {
47
79
  this.messageKeys = messageKeys;
48
80
  this.messages = {};
49
81
  this.localeImportMap = localeImportMap;
50
82
  this.supportedLocales = Object.keys(localeImportMap);
83
+ this.defaultLocale = defaultLocale;
51
84
 
52
85
  this._localeLoadingPromises = {};
53
86
  this._parseCache = new Map();
54
87
  this._subscribers = new Set();
55
88
 
89
+ this._loadLocale(this.defaultLocale);
56
90
  this.fallbackMessage = new InternalIntlMessage('THIS MESSAGE FAILED TO LOAD', 'en-US');
91
+
92
+ // In cases where hot module replacement is available, set up cache clearing whenever the
93
+ // targets change so that values are always replaced.
94
+ // @ts-expect-error `hot` not defined in types.
95
+ if (module.hot) {
96
+ for (const [locale, file] of Object.entries(localeImportMap)) {
97
+ // @ts-expect-error `hot` not defined in types.
98
+ module.hot.accept(file, () => {
99
+ this._parseCache.clear();
100
+ this._loadLocale(locale);
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Provide additional debug information to use during development, providing additional context
108
+ * for console error messages.
109
+ *
110
+ * @param {Record<string, string>} keyMap
111
+ * @param {Record<string, string>} localeFileMap
112
+ */
113
+ withDebugValues(keyMap: Record<string, string>, localeFileMap: Record<string, string>) {
114
+ this._debugKeyMap = keyMap;
115
+ this._localeFileMap = localeFileMap;
57
116
  }
58
117
 
59
118
  get(key: string, locale: LocaleId): InternalIntlMessage {
119
+ const value =
120
+ this.getMessageValue(key, locale) ?? this.getMessageValue(key, this.defaultLocale);
121
+ if (value != null) {
122
+ return value;
123
+ }
124
+
125
+ // If the message couldn't be found in either the requested nor the default locale, then
126
+ // nothing can be done.
127
+ const errorKey = this._debugKeyMap != null ? `"${this._debugKeyMap[key]}" (${key})` : undefined;
128
+ const requestedLocale =
129
+ this._localeFileMap != null ? `${locale} (${this._localeFileMap[locale]})` : locale;
130
+ const defaultLocale =
131
+ this._localeFileMap != null
132
+ ? `${this.defaultLocale} (${this._localeFileMap[this.defaultLocale]})`
133
+ : this.defaultLocale;
134
+ console.warn(
135
+ `Requested message ${errorKey} does not have a value in the requested locale ${requestedLocale} nor the default locale ${defaultLocale}`,
136
+ );
137
+ return this.fallbackMessage;
138
+ }
139
+
140
+ /**
141
+ * Return the value of the message with the given `key` in the given `locale`. If the message has
142
+ * no value in that locale, this function returns `undefined`.
143
+ *
144
+ * This function will first check the cache to see if it has already been loaded, and will later
145
+ * set the cache after parsing if not. If the requested locale has not yet been loaded, this
146
+ * function will trigger a load for that locale, but will not wait for it to resolve before
147
+ * returning.
148
+ */
149
+ getMessageValue(key: string, locale: LocaleId): InternalIntlMessage | undefined {
60
150
  const cacheKey = key + '@' + locale;
61
151
  const cachedValue = this._parseCache.get(cacheKey);
62
152
  if (cachedValue != null) return cachedValue;
63
153
 
154
+ // Return early if this locale is still in the process of being loaded.
155
+ if (this._localeLoadingPromises[locale] != null) {
156
+ return undefined;
157
+ }
158
+
159
+ // If not, check whether it's been loaded and trigger a load if not.
64
160
  if (this.messages[locale] == null) {
65
161
  // Ensure the locale is loaded, if it is supported
66
162
  if (this.supportedLocales.includes(locale)) {
67
163
  this._loadLocale(locale);
68
164
  }
69
165
 
70
- // Otherwise, fallback to just the key value in the meantime.
71
- return this.fallbackMessage;
166
+ return undefined;
72
167
  }
73
168
 
74
- // Then try to return the loaded message
169
+ // Then try to return the loaded message.
75
170
  if (key in this.messages[locale]) {
76
171
  const content = this.messages[locale][key];
77
172
  const message = new InternalIntlMessage(content, locale);
@@ -79,10 +174,8 @@ export class MessageLoader {
79
174
  return message;
80
175
  }
81
176
 
82
- // And finally throw if there's no match whatsoever
83
- throw new Error(
84
- `Requested message ${key} does not have a value in the requested locale nor the default locale`,
85
- );
177
+ // Otherwise just assume it doesn't exist.
178
+ return undefined;
86
179
  }
87
180
 
88
181
  /**
@@ -93,7 +186,7 @@ export class MessageLoader {
93
186
  */
94
187
  getBinds(): Record<string, IntlMessageGetter> {
95
188
  const onChange = this.onChange.bind(this);
96
- return this.messageKeys.reduce(
189
+ return Object.keys(this.messageKeys).reduce(
97
190
  (acc, key) => {
98
191
  const bound = this.get.bind(this, key) as IntlMessageGetter;
99
192
  bound.onChange = onChange;
@@ -122,12 +215,10 @@ export class MessageLoader {
122
215
  // If the locale is already set in `messages`, then it doesn't need to be loaded again.
123
216
  if (this.messages[locale] != null) return;
124
217
 
125
- const loadStart = performance.now();
126
218
  const loadingPromise = this.localeImportMap[locale]();
127
219
  this._localeLoadingPromises[locale] = loadingPromise;
128
220
  this.messages[locale] = (await loadingPromise).default;
129
221
  delete this._localeLoadingPromises[locale];
130
- const loadEnd = performance.now();
131
222
  this.emitChange();
132
223
  }
133
224