@adrienlcp/i18n 0.1.0 → 0.1.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.
@@ -25,6 +25,18 @@ type Localized<Reference> = Dictionary & DictionaryFor<Reference>;
25
25
  type AnyLoader = () => Promise<{
26
26
  default: Dictionary;
27
27
  }>;
28
+ /**
29
+ * Refuses a `defaultLocale` typed as more than one locale. `const
30
+ * DEFAULT_LOCALE: Locale = 'en'` holds `'en'` but is typed `'en' | 'fr'`, which
31
+ * would make the reference every locale's dictionary at once: a second locale's
32
+ * leaves are plain `string`s, so every key with a placeholder would come out as
33
+ * `never` at the call site, far from the cause. The property name is the error
34
+ * TypeScript prints here instead.
35
+ */
36
+ type SingleLocale<DefaultLocale extends string> = IsUnion<DefaultLocale> extends false ? unknown : {
37
+ 'defaultLocale must be typed as one locale, not a union: write `const DEFAULT_LOCALE = "en" satisfies Locale`': never;
38
+ };
39
+ type IsUnion<Member, Whole = Member> = Member extends unknown ? [Whole] extends [Member] ? false : true : never;
28
40
  /**
29
41
  * One locale's entry held to the reference, whichever way it arrives. A loader
30
42
  * is compared through the dictionary its module `export default`s, so a locale
@@ -110,7 +122,7 @@ export type I18n<Reference, Locale extends string, DefaultLocale extends Locale
110
122
  * `load` resolves on the spot.
111
123
  */
112
124
  export declare const createI18n: <const Entries extends Record<string, Dictionary | AnyLoader>, const DefaultLocale extends keyof Entries & string>({ defaultLocale, dictionaries }: {
113
- defaultLocale: DefaultLocale;
125
+ defaultLocale: DefaultLocale & SingleLocale<DefaultLocale>;
114
126
  dictionaries: Entries & { [Locale in keyof Entries]: Matching<Entries[DefaultLocale], Entries[Locale]>; } & Record<DefaultLocale, Localized<Entries[DefaultLocale]>>;
115
127
  }) => I18n<Entries[DefaultLocale], keyof Entries & string, DefaultLocale>;
116
128
  export {};
@@ -38,6 +38,11 @@ export declare const defineDictionary: <const T extends { [Segment in keyof T]:
38
38
  * TypeScript's excess property check does the other half of the work: a key the
39
39
  * reference does not have is rejected at the literal, at every depth.
40
40
  *
41
+ * Only for a dictionary handed to `createTranslator` directly. One registered
42
+ * with `createI18n` is written through `defineDictionary`, like the reference:
43
+ * the annotation widens each message to `string`, and the registry, which
44
+ * compares placeholders, then refuses every message that has one.
45
+ *
41
46
  * ```ts
42
47
  * export const FR_DICTIONARY: DictionaryFor<typeof EN_DICTIONARY> = { … }
43
48
  * ```
@@ -106,9 +106,10 @@ const FORMATTED_COUNT = '{?}';
106
106
  * That is dictionary text rather than a caller's value, so the rule above is
107
107
  * untouched.
108
108
  *
109
- * A value of the wrong type, or one the message never asked for, leaves its
110
- * placeholder standing rather than throwing: one bad value costs one word, not
111
- * the whole sentence.
109
+ * A value of the wrong type, one the message never asked for, or one the
110
+ * locale cannot print — an invalid `Date`, an infinite count, a code that names
111
+ * no language — leaves its placeholder standing rather than throwing: one bad
112
+ * value costs one word, not the whole sentence.
112
113
  */
113
114
  const substitute = ({ expanding = [], formatters, message, options, values }) => message.replace(PLACEHOLDER, (placeholder, name, type) => {
114
115
  const value = values[name];
@@ -117,7 +118,7 @@ const substitute = ({ expanding = [], formatters, message, options, values }) =>
117
118
  }
118
119
  switch (type) {
119
120
  case 'date':
120
- return value instanceof Date
121
+ return isValidDate(value)
121
122
  ? formatters.date(options.date?.[name]).format(value)
122
123
  : placeholder;
123
124
  case 'displayname':
@@ -167,7 +168,7 @@ const substitute = ({ expanding = [], formatters, message, options, values }) =>
167
168
  })
168
169
  : placeholder;
169
170
  case 'relative':
170
- return typeof value === 'number'
171
+ return typeof value === 'number' && Number.isFinite(value)
171
172
  ? (relativeTime({
172
173
  count: value,
173
174
  formatters,
@@ -220,9 +221,21 @@ const pluralize = ({ count, formatters, forms }) => {
220
221
  /**
221
222
  * A message declaring `{x:displayname}` must declare the kind of name it wants,
222
223
  * so the options are never absent — but the runtime shape stays loose, and
223
- * `Intl.DisplayNames` throws without a `type`. Nothing rather than a crash.
224
+ * `Intl.DisplayNames` throws without a `type`, and on a string that is no code
225
+ * at all (`'not a language'`). Nothing rather than a crash.
224
226
  */
225
- const displayName = ({ formatters, of, options }) => options === undefined ? undefined : formatters.displayname(options).of(of);
227
+ const displayName = ({ formatters, of, options }) => {
228
+ if (options === undefined) {
229
+ return undefined;
230
+ }
231
+ try {
232
+ return formatters.displayname(options).of(of);
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ };
238
+ const isValidDate = (value) => value instanceof Date && !Number.isNaN(value.getTime());
226
239
  /** Same reasoning: the unit is declared with the message, or there is none. */
227
240
  const relativeTime = ({ count, formatters, unit }) => unit === undefined
228
241
  ? undefined
package/package.json CHANGED
@@ -24,13 +24,12 @@
24
24
  "type": "git",
25
25
  "url": "git+https://github.com/AdrienLcp/packages.git"
26
26
  },
27
+ "sideEffects": false,
28
+ "type": "module",
29
+ "version": "0.1.2",
27
30
  "scripts": {
28
31
  "build": "tsc -p tsconfig.build.json",
29
- "prepublishOnly": "pnpm build",
30
32
  "test": "vitest run",
31
33
  "test:watch": "vitest --watch"
32
- },
33
- "sideEffects": false,
34
- "type": "module",
35
- "version": "0.1.0"
36
- }
34
+ }
35
+ }