@lakutata/i18n 3.0.0-beta.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.
Files changed (80) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +347 -0
  3. package/dist/cjs/exports/I18n.d.ts +15 -0
  4. package/dist/cjs/exports/I18n.js +45 -0
  5. package/dist/cjs/exports/I18nMiddleware.d.ts +2 -0
  6. package/dist/cjs/exports/I18nMiddleware.js +21 -0
  7. package/dist/cjs/i18n/I18n.d.ts +313 -0
  8. package/dist/cjs/i18n/I18n.js +462 -0
  9. package/dist/cjs/i18n/Translator.d.ts +124 -0
  10. package/dist/cjs/i18n/Translator.js +156 -0
  11. package/dist/cjs/i18n/exceptions/I18nCatalogException.d.ts +15 -0
  12. package/dist/cjs/i18n/exceptions/I18nCatalogException.js +22 -0
  13. package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
  14. package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.js +21 -0
  15. package/dist/cjs/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
  16. package/dist/cjs/i18n/exceptions/I18nMissingKeyException.js +19 -0
  17. package/dist/cjs/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
  18. package/dist/cjs/i18n/exceptions/I18nMissingParameterException.js +20 -0
  19. package/dist/cjs/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
  20. package/dist/cjs/i18n/exceptions/I18nTranslatorException.js +19 -0
  21. package/dist/cjs/i18n/interfaces/I18nOptions.d.ts +70 -0
  22. package/dist/cjs/i18n/interfaces/I18nOptions.js +2 -0
  23. package/dist/cjs/i18n/interfaces/ITranslator.d.ts +57 -0
  24. package/dist/cjs/i18n/interfaces/ITranslator.js +2 -0
  25. package/dist/cjs/i18n/lib/Catalog.d.ts +34 -0
  26. package/dist/cjs/i18n/lib/Catalog.js +104 -0
  27. package/dist/cjs/i18n/lib/CatalogTools.d.ts +86 -0
  28. package/dist/cjs/i18n/lib/CatalogTools.js +218 -0
  29. package/dist/cjs/i18n/lib/Locale.d.ts +95 -0
  30. package/dist/cjs/i18n/lib/Locale.js +182 -0
  31. package/dist/cjs/i18n/lib/LocaleTranslator.d.ts +93 -0
  32. package/dist/cjs/i18n/lib/LocaleTranslator.js +105 -0
  33. package/dist/cjs/i18n/lib/MessageFormat.d.ts +144 -0
  34. package/dist/cjs/i18n/lib/MessageFormat.js +372 -0
  35. package/dist/cjs/i18n/middleware/CommanderTranslator.d.ts +71 -0
  36. package/dist/cjs/i18n/middleware/CommanderTranslator.js +69 -0
  37. package/dist/cjs/i18n/middleware/I18nMiddleware.d.ts +126 -0
  38. package/dist/cjs/i18n/middleware/I18nMiddleware.js +210 -0
  39. package/dist/cjs/i18n/types/I18nMessages.d.ts +24 -0
  40. package/dist/cjs/i18n/types/I18nMessages.js +2 -0
  41. package/dist/cjs/package.json +1 -0
  42. package/dist/esm/exports/I18n.js +14 -0
  43. package/dist/esm/exports/I18nMiddleware.js +2 -0
  44. package/dist/esm/i18n/I18n.js +454 -0
  45. package/dist/esm/i18n/Translator.js +153 -0
  46. package/dist/esm/i18n/exceptions/I18nCatalogException.js +18 -0
  47. package/dist/esm/i18n/exceptions/I18nMessageSyntaxException.js +17 -0
  48. package/dist/esm/i18n/exceptions/I18nMissingKeyException.js +15 -0
  49. package/dist/esm/i18n/exceptions/I18nMissingParameterException.js +16 -0
  50. package/dist/esm/i18n/exceptions/I18nTranslatorException.js +15 -0
  51. package/dist/esm/i18n/interfaces/I18nOptions.js +1 -0
  52. package/dist/esm/i18n/interfaces/ITranslator.js +1 -0
  53. package/dist/esm/i18n/lib/Catalog.js +96 -0
  54. package/dist/esm/i18n/lib/CatalogTools.js +213 -0
  55. package/dist/esm/i18n/lib/Locale.js +173 -0
  56. package/dist/esm/i18n/lib/LocaleTranslator.js +101 -0
  57. package/dist/esm/i18n/lib/MessageFormat.js +367 -0
  58. package/dist/esm/i18n/middleware/CommanderTranslator.js +66 -0
  59. package/dist/esm/i18n/middleware/I18nMiddleware.js +206 -0
  60. package/dist/esm/i18n/types/I18nMessages.js +1 -0
  61. package/dist/types/exports/I18n.d.ts +15 -0
  62. package/dist/types/exports/I18nMiddleware.d.ts +2 -0
  63. package/dist/types/i18n/I18n.d.ts +313 -0
  64. package/dist/types/i18n/Translator.d.ts +124 -0
  65. package/dist/types/i18n/exceptions/I18nCatalogException.d.ts +15 -0
  66. package/dist/types/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
  67. package/dist/types/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
  68. package/dist/types/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
  69. package/dist/types/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
  70. package/dist/types/i18n/interfaces/I18nOptions.d.ts +70 -0
  71. package/dist/types/i18n/interfaces/ITranslator.d.ts +57 -0
  72. package/dist/types/i18n/lib/Catalog.d.ts +34 -0
  73. package/dist/types/i18n/lib/CatalogTools.d.ts +86 -0
  74. package/dist/types/i18n/lib/Locale.d.ts +95 -0
  75. package/dist/types/i18n/lib/LocaleTranslator.d.ts +93 -0
  76. package/dist/types/i18n/lib/MessageFormat.d.ts +144 -0
  77. package/dist/types/i18n/middleware/CommanderTranslator.d.ts +71 -0
  78. package/dist/types/i18n/middleware/I18nMiddleware.d.ts +126 -0
  79. package/dist/types/i18n/types/I18nMessages.d.ts +24 -0
  80. package/package.json +60 -0
@@ -0,0 +1,93 @@
1
+ import type { I18n } from '../I18n.js';
2
+ import type { ITranslator } from '../interfaces/ITranslator.js';
3
+ import type { MessageKey, MessageParams } from '../types/I18nMessages.js';
4
+ /**
5
+ * The messages of one language, without giving the language at each call: {@link I18n.translator} creates one, to use
6
+ * out of a request (a schedule, a singleton component, a background job). Its methods call the component's methods
7
+ * with its language, so the messages added later with {@link I18n.addMessages} are seen too. In a request, inject the
8
+ * {@link Translator} instead.
9
+ * @example
10
+ * ```typescript
11
+ * import {Application} from 'lakutata'
12
+ * import {buildI18nOptions, type I18n, type ITranslator} from 'lakutata/com/i18n'
13
+ *
14
+ * Application
15
+ * .run(() => ({
16
+ * id: 'i18n.app',
17
+ * name: 'I18n',
18
+ * components: {
19
+ * i18n: buildI18nOptions({
20
+ * messages: {
21
+ * 'en': {order: {count: '{count, plural, =0 {No orders} one {# order} other {# orders}}'}},
22
+ * 'de': {order: {count: '{count, plural, =0 {Keine Bestellungen} one {# Bestellung} other {# Bestellungen}}'}}
23
+ * }
24
+ * })
25
+ * }
26
+ * }))
27
+ * .onLaunched(async (app: Application): Promise<void> => {
28
+ * const i18n: I18n = await app.getObject('i18n')
29
+ * const german: ITranslator = i18n.translator('de-DE')
30
+ * console.log(german.locale) //de-DE, its messages looked up in de-DE, then de, then en
31
+ * console.log(german.t('order.count', {count: 1000})) //1.000 Bestellungen
32
+ * console.log(german.number(1234.5)) //1.234,5
33
+ * })
34
+ * ```
35
+ */
36
+ export declare class LocaleTranslator implements ITranslator {
37
+ protected readonly i18n: I18n;
38
+ readonly locale: string;
39
+ /**
40
+ * Create the translator of a language; {@link I18n.translator} does it, normalizing the language first.
41
+ * @param i18n The component holding the messages.
42
+ * @param locale The language tag, canonical.
43
+ */
44
+ constructor(i18n: I18n, locale: string);
45
+ /**
46
+ * Give the message of a key in the translator's language, formatted with its arguments (see {@link I18n.t}).
47
+ * @param key The key of the message (`order.count`).
48
+ * @param params The arguments of the message, by their names.
49
+ * @returns The formatted message. With `missing: 'key'`, the key itself when it is missing, `{name}` in place of a
50
+ * missing argument, a plain number for a currency argument without a currency (each with a warning, once).
51
+ * @throws {I18nMissingKeyException} When no message has the key and the `missing` option is `'throw'`.
52
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
53
+ * the `missing` option is `'throw'`.
54
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
55
+ */
56
+ t<K extends MessageKey>(key: K, params?: MessageParams<K>): string;
57
+ /**
58
+ * Tell whether a key has a message in the translator's language or in the languages it falls back to.
59
+ * @param key The key of the message.
60
+ * @returns `true` when {@link LocaleTranslator.t} finds a message for the key.
61
+ */
62
+ has(key: string): boolean;
63
+ /**
64
+ * Format an ICU MessageFormat message which is not in the catalogs, in the translator's language.
65
+ * @param message The message (`Hello, {name}`).
66
+ * @param params The arguments of the message, by their names.
67
+ * @returns The formatted message. With `missing: 'key'`, `{name}` in place of a missing argument, a plain number
68
+ * for a currency argument without a currency (each with a warning, once).
69
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
70
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
71
+ * the `missing` option is `'throw'`.
72
+ */
73
+ format(message: string, params?: Record<string, unknown>): string;
74
+ /**
75
+ * Format a number in the translator's language (`1234.5` gives `1,234.5` in English, `1.234,5` in German).
76
+ * @param value The number.
77
+ * @param options The options of `Intl.NumberFormat` (`{style: 'currency', currency: 'EUR'}`); the component's
78
+ * `currency` option is not applied here.
79
+ * @returns The formatted number.
80
+ * @throws {RangeError} When the options are invalid (a currency style without its currency).
81
+ */
82
+ number(value: number | bigint, options?: Intl.NumberFormatOptions): string;
83
+ /**
84
+ * Format a date (and its time when the options ask for it) in the translator's language, in the component's
85
+ * `timeZone` (the process's one without it).
86
+ * @param value The date, or a timestamp in milliseconds.
87
+ * @param options The options of `Intl.DateTimeFormat` (`{dateStyle: 'long'}`); their `timeZone` replaces the
88
+ * component's one.
89
+ * @returns The formatted date.
90
+ * @throws {RangeError} When the date is invalid or the options are.
91
+ */
92
+ date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
93
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocaleTranslator = void 0;
4
+ /**
5
+ * The messages of one language, without giving the language at each call: {@link I18n.translator} creates one, to use
6
+ * out of a request (a schedule, a singleton component, a background job). Its methods call the component's methods
7
+ * with its language, so the messages added later with {@link I18n.addMessages} are seen too. In a request, inject the
8
+ * {@link Translator} instead.
9
+ * @example
10
+ * ```typescript
11
+ * import {Application} from 'lakutata'
12
+ * import {buildI18nOptions, type I18n, type ITranslator} from 'lakutata/com/i18n'
13
+ *
14
+ * Application
15
+ * .run(() => ({
16
+ * id: 'i18n.app',
17
+ * name: 'I18n',
18
+ * components: {
19
+ * i18n: buildI18nOptions({
20
+ * messages: {
21
+ * 'en': {order: {count: '{count, plural, =0 {No orders} one {# order} other {# orders}}'}},
22
+ * 'de': {order: {count: '{count, plural, =0 {Keine Bestellungen} one {# Bestellung} other {# Bestellungen}}'}}
23
+ * }
24
+ * })
25
+ * }
26
+ * }))
27
+ * .onLaunched(async (app: Application): Promise<void> => {
28
+ * const i18n: I18n = await app.getObject('i18n')
29
+ * const german: ITranslator = i18n.translator('de-DE')
30
+ * console.log(german.locale) //de-DE, its messages looked up in de-DE, then de, then en
31
+ * console.log(german.t('order.count', {count: 1000})) //1.000 Bestellungen
32
+ * console.log(german.number(1234.5)) //1.234,5
33
+ * })
34
+ * ```
35
+ */
36
+ class LocaleTranslator {
37
+ /**
38
+ * Create the translator of a language; {@link I18n.translator} does it, normalizing the language first.
39
+ * @param i18n The component holding the messages.
40
+ * @param locale The language tag, canonical.
41
+ */
42
+ constructor(i18n, locale) {
43
+ this.i18n = i18n;
44
+ this.locale = locale;
45
+ }
46
+ /**
47
+ * Give the message of a key in the translator's language, formatted with its arguments (see {@link I18n.t}).
48
+ * @param key The key of the message (`order.count`).
49
+ * @param params The arguments of the message, by their names.
50
+ * @returns The formatted message. With `missing: 'key'`, the key itself when it is missing, `{name}` in place of a
51
+ * missing argument, a plain number for a currency argument without a currency (each with a warning, once).
52
+ * @throws {I18nMissingKeyException} When no message has the key and the `missing` option is `'throw'`.
53
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
54
+ * the `missing` option is `'throw'`.
55
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
56
+ */
57
+ t(key, params) {
58
+ return this.i18n.t(key, params, this.locale);
59
+ }
60
+ /**
61
+ * Tell whether a key has a message in the translator's language or in the languages it falls back to.
62
+ * @param key The key of the message.
63
+ * @returns `true` when {@link LocaleTranslator.t} finds a message for the key.
64
+ */
65
+ has(key) {
66
+ return this.i18n.has(key, this.locale);
67
+ }
68
+ /**
69
+ * Format an ICU MessageFormat message which is not in the catalogs, in the translator's language.
70
+ * @param message The message (`Hello, {name}`).
71
+ * @param params The arguments of the message, by their names.
72
+ * @returns The formatted message. With `missing: 'key'`, `{name}` in place of a missing argument, a plain number
73
+ * for a currency argument without a currency (each with a warning, once).
74
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
75
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
76
+ * the `missing` option is `'throw'`.
77
+ */
78
+ format(message, params) {
79
+ return this.i18n.format(message, params, this.locale);
80
+ }
81
+ /**
82
+ * Format a number in the translator's language (`1234.5` gives `1,234.5` in English, `1.234,5` in German).
83
+ * @param value The number.
84
+ * @param options The options of `Intl.NumberFormat` (`{style: 'currency', currency: 'EUR'}`); the component's
85
+ * `currency` option is not applied here.
86
+ * @returns The formatted number.
87
+ * @throws {RangeError} When the options are invalid (a currency style without its currency).
88
+ */
89
+ number(value, options) {
90
+ return new Intl.NumberFormat(this.locale, options).format(value);
91
+ }
92
+ /**
93
+ * Format a date (and its time when the options ask for it) in the translator's language, in the component's
94
+ * `timeZone` (the process's one without it).
95
+ * @param value The date, or a timestamp in milliseconds.
96
+ * @param options The options of `Intl.DateTimeFormat` (`{dateStyle: 'long'}`); their `timeZone` replaces the
97
+ * component's one.
98
+ * @returns The formatted date.
99
+ * @throws {RangeError} When the date is invalid or the options are.
100
+ */
101
+ date(value, options) {
102
+ return new Intl.DateTimeFormat(this.locale, { timeZone: this.i18n.getTimeZone(), ...options }).format(value);
103
+ }
104
+ }
105
+ exports.LocaleTranslator = LocaleTranslator;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * A part of a message parsed by {@link parseMessage}: a literal text (a string, its quoting already removed), a plain
3
+ * argument (`{name}`), a number argument (`{n, number, percent}`), a date or time argument (`{at, date, short}`), a
4
+ * choice (`plural`, `selectordinal`, `select`), or the `#` of a plural (its number).
5
+ */
6
+ export type MessagePart = string | {
7
+ /**
8
+ * A plain argument, `{name}`: a number formatted in the language, a `Date` as a date, anything else as a string.
9
+ */
10
+ type: 'argument';
11
+ /**
12
+ * The name of the argument.
13
+ */
14
+ name: string;
15
+ } | {
16
+ /**
17
+ * A number argument, `{name, number}` or `{name, number, style}`.
18
+ */
19
+ type: 'number';
20
+ /**
21
+ * The name of the argument.
22
+ */
23
+ name: string;
24
+ /**
25
+ * The style: `integer`, `percent`, `currency` or `currency/XXX` (an ISO 4217 code); absent for a plain number.
26
+ */
27
+ style?: string;
28
+ } | {
29
+ /**
30
+ * A date argument (`{name, date, style}`) or a time argument (`{name, time, style}`).
31
+ */
32
+ type: 'date' | 'time';
33
+ /**
34
+ * The name of the argument.
35
+ */
36
+ name: string;
37
+ /**
38
+ * The style: `short`, `medium`, `long` or `full`; absent for `medium`.
39
+ */
40
+ style?: string;
41
+ } | {
42
+ /**
43
+ * A cardinal (`plural`) or ordinal (`selectordinal`) choice on a number argument.
44
+ */
45
+ type: 'plural' | 'selectordinal';
46
+ /**
47
+ * The name of the argument.
48
+ */
49
+ name: string;
50
+ /**
51
+ * The `offset:` of the choice, subtracted from the number before its plural rule and its `#`; 0 without it.
52
+ */
53
+ offset: number;
54
+ /**
55
+ * The messages by selector: `=0` (an exact value, matched before the offset), a plural category of the
56
+ * language (`zero`, `one`, `two`, `few`, `many`), and the required `other`.
57
+ */
58
+ options: Record<string, MessagePart[]>;
59
+ } | {
60
+ /**
61
+ * A choice on the string value of an argument, `{name, select, admin {...} other {...}}`.
62
+ */
63
+ type: 'select';
64
+ /**
65
+ * The name of the argument.
66
+ */
67
+ name: string;
68
+ /**
69
+ * The messages by value of the argument, and the required `other`.
70
+ */
71
+ options: Record<string, MessagePart[]>;
72
+ } | {
73
+ /**
74
+ * The `#` of a plural: the number of the closest enclosing `plural` or `selectordinal` (minus its offset),
75
+ * formatted in the language.
76
+ */
77
+ type: 'pound';
78
+ };
79
+ /**
80
+ * How a message is formatted
81
+ */
82
+ export type FormatContext = {
83
+ locale: string;
84
+ /**
85
+ * The currency of {price, number, currency} (ISO 4217)
86
+ */
87
+ currency?: string;
88
+ /**
89
+ * The time zone of the dates and the times (IANA)
90
+ */
91
+ timeZone?: string;
92
+ /**
93
+ * An argument or a currency missing: thrown, or kept (the argument as it is written, {name}; the number without
94
+ * currency) with a warning
95
+ */
96
+ missing: 'throw' | 'keep';
97
+ /**
98
+ * Told what is kept when missing is 'keep'
99
+ */
100
+ warn?: (warning: string) => void;
101
+ };
102
+ /**
103
+ * Parse an ICU MessageFormat message into its parts, to inspect it (its arguments: {@link messageArguments}). The
104
+ * syntax: `{name}`; `{name, number}` with the styles `integer`, `percent`, `currency`, `currency/XXX`; `{name, date}`
105
+ * and `{name, time}` with the styles `short`, `medium`, `long`, `full`; `{name, plural, ...}` (with `offset:n`),
106
+ * `{name, selectordinal, ...}` and `{name, select, ...}`, each requiring an `other` option; `#` in a plural; `''` for
107
+ * an apostrophe and `'{...}'` to quote the braces (and `#` in a plural). Other argument types and the skeletons
108
+ * (`::currency/EUR`) are not supported.
109
+ * @param message The message.
110
+ * @param source Where the message comes from (`zh-CN: order.count`), the prefix of the error messages: `'message'` by
111
+ * default.
112
+ * @returns The parts of the message.
113
+ * @throws {I18nMessageSyntaxException} When the message is not valid, with the source and the position.
114
+ * @example
115
+ * ```typescript
116
+ * import {parseMessage, type MessagePart} from 'lakutata/com/i18n'
117
+ *
118
+ * const parts: MessagePart[] = parseMessage('{count, plural, one {# order} other {# orders}} for {customer}')
119
+ * console.log(parts.length) //3
120
+ * ```
121
+ */
122
+ export declare function parseMessage(message: string, source?: string): MessagePart[];
123
+ /**
124
+ * List the arguments a parsed message uses, nested choices included, with the type of their first use: `argument`,
125
+ * `number`, `date`, `time`, `plural`, `selectordinal` or `select`.
126
+ * @param parts The parts of the message, from {@link parseMessage}.
127
+ * @returns The argument types by argument name, in their order in the message.
128
+ * @example
129
+ * ```typescript
130
+ * import {messageArguments, parseMessage} from 'lakutata/com/i18n'
131
+ *
132
+ * const args: Map<string, string> = messageArguments(parseMessage('{count, plural, other {# orders}} for {customer}'))
133
+ * console.log([...args]) //[['count', 'plural'], ['customer', 'argument']]
134
+ * ```
135
+ */
136
+ export declare function messageArguments(parts: MessagePart[]): Map<string, string>;
137
+ /**
138
+ * Format a parsed message
139
+ * @param parts
140
+ * @param params
141
+ * @param context
142
+ * @param source where the message is (its key), for the errors
143
+ */
144
+ export declare function formatMessage(parts: MessagePart[], params: Record<string, unknown>, context: FormatContext, source?: string): string;