@svelstack/translator 0.9.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/dist/index.js ADDED
@@ -0,0 +1,410 @@
1
+ // ../internal/utils.js
2
+ var numberOfPluralParts = {
3
+ "af": 2,
4
+ "bn": 2,
5
+ "bg": 2,
6
+ "ca": 2,
7
+ "da": 2,
8
+ "de": 2,
9
+ "el": 2,
10
+ "en": 2,
11
+ "eo": 2,
12
+ "es": 2,
13
+ "et": 2,
14
+ "eu": 2,
15
+ "fa": 2,
16
+ "fi": 2,
17
+ "fo": 2,
18
+ "fur": 2,
19
+ "fy": 2,
20
+ "gl": 2,
21
+ "gu": 2,
22
+ "ha": 2,
23
+ "he": 2,
24
+ "hu": 2,
25
+ "is": 2,
26
+ "it": 2,
27
+ "ku": 2,
28
+ "lb": 2,
29
+ "ml": 2,
30
+ "mn": 2,
31
+ "mr": 2,
32
+ "nah": 2,
33
+ "nb": 2,
34
+ "ne": 2,
35
+ "nl": 2,
36
+ "nn": 2,
37
+ "no": 2,
38
+ "oc": 2,
39
+ "om": 2,
40
+ "or": 2,
41
+ "pa": 2,
42
+ "pap": 2,
43
+ "ps": 2,
44
+ "pt": 2,
45
+ "so": 2,
46
+ "sq": 2,
47
+ "sv": 2,
48
+ "sw": 2,
49
+ "ta": 2,
50
+ "te": 2,
51
+ "tk": 2,
52
+ "ur": 2,
53
+ "zu": 2,
54
+ "am": 2,
55
+ "bh": 2,
56
+ "fil": 2,
57
+ "fr": 2,
58
+ "gun": 2,
59
+ "hi": 2,
60
+ "hy": 2,
61
+ "ln": 2,
62
+ "mg": 2,
63
+ "nso": 2,
64
+ "pt_BR": 2,
65
+ "ti": 2,
66
+ "wa": 2,
67
+ "mk": 2,
68
+ "be": 3,
69
+ "bs": 3,
70
+ "hr": 3,
71
+ "ru": 3,
72
+ "sh": 3,
73
+ "sr": 3,
74
+ "uk": 3,
75
+ "cs": 3,
76
+ "sk": 3,
77
+ "ga": 3,
78
+ "lt": 3,
79
+ "lv": 3,
80
+ "pl": 3,
81
+ "ro": 3,
82
+ "sl": 4,
83
+ "mt": 4,
84
+ "cy": 4,
85
+ "ar": 6
86
+ };
87
+ function getNumberOfPluralParts(locale) {
88
+ const normalizedLocale = locale !== "pt_BR" && locale.length > 3 ? locale.substring(0, locale.lastIndexOf("_")) : locale;
89
+ return numberOfPluralParts[normalizedLocale] ?? 0;
90
+ }
91
+ function getLocaleNumber(locale, number) {
92
+ number = Math.abs(number);
93
+ const normalizedLocale = locale !== "pt_BR" && locale.length > 3 ? locale.substring(0, locale.lastIndexOf("_")) : locale;
94
+ switch (normalizedLocale) {
95
+ case "af":
96
+ case "bn":
97
+ case "bg":
98
+ case "ca":
99
+ case "da":
100
+ case "de":
101
+ case "el":
102
+ case "en":
103
+ case "eo":
104
+ case "es":
105
+ case "et":
106
+ case "eu":
107
+ case "fa":
108
+ case "fi":
109
+ case "fo":
110
+ case "fur":
111
+ case "fy":
112
+ case "gl":
113
+ case "gu":
114
+ case "ha":
115
+ case "he":
116
+ case "hu":
117
+ case "is":
118
+ case "it":
119
+ case "ku":
120
+ case "lb":
121
+ case "ml":
122
+ case "mn":
123
+ case "mr":
124
+ case "nah":
125
+ case "nb":
126
+ case "ne":
127
+ case "nl":
128
+ case "nn":
129
+ case "no":
130
+ case "oc":
131
+ case "om":
132
+ case "or":
133
+ case "pa":
134
+ case "pap":
135
+ case "ps":
136
+ case "pt":
137
+ case "so":
138
+ case "sq":
139
+ case "sv":
140
+ case "sw":
141
+ case "ta":
142
+ case "te":
143
+ case "tk":
144
+ case "ur":
145
+ case "zu":
146
+ return number === 1 ? 0 : 1;
147
+ case "am":
148
+ case "bh":
149
+ case "fil":
150
+ case "fr":
151
+ case "gun":
152
+ case "hi":
153
+ case "hy":
154
+ case "ln":
155
+ case "mg":
156
+ case "nso":
157
+ case "pt_BR":
158
+ case "ti":
159
+ case "wa":
160
+ return number < 2 ? 0 : 1;
161
+ case "be":
162
+ case "bs":
163
+ case "hr":
164
+ case "ru":
165
+ case "sh":
166
+ case "sr":
167
+ case "uk":
168
+ return number % 10 === 1 && number % 100 !== 11 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
169
+ case "cs":
170
+ case "sk":
171
+ return number === 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
172
+ case "ga":
173
+ return number === 1 ? 0 : number === 2 ? 1 : 2;
174
+ case "lt":
175
+ return number % 10 === 1 && number % 100 !== 11 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
176
+ case "sl":
177
+ return number % 100 === 1 ? 0 : number % 100 === 2 ? 1 : number % 100 === 3 || number % 100 === 4 ? 2 : 3;
178
+ case "mk":
179
+ return number % 10 === 1 ? 0 : 1;
180
+ case "mt":
181
+ return number === 1 ? 0 : number === 0 || number % 100 > 1 && number % 100 < 11 ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;
182
+ case "lv":
183
+ return number === 0 ? 0 : number % 10 === 1 && number % 100 !== 11 ? 1 : 2;
184
+ case "pl":
185
+ return number === 1 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;
186
+ case "cy":
187
+ return number === 1 ? 0 : number === 2 ? 1 : number === 8 || number === 11 ? 2 : 3;
188
+ case "ro":
189
+ return number === 1 ? 0 : number === 0 || number % 100 > 0 && number % 100 < 20 ? 1 : 2;
190
+ case "ar":
191
+ return number === 0 ? 0 : number === 1 ? 1 : number === 2 ? 2 : number % 100 >= 3 && number % 100 <= 10 ? 3 : number % 100 >= 11 && number % 100 <= 99 ? 4 : 5;
192
+ default:
193
+ return 0;
194
+ }
195
+ }
196
+
197
+ // src/formatter.js
198
+ var ChainMessageFormatter = class {
199
+ /**
200
+ * @type {MessageFormatter[]}
201
+ */
202
+ #formatters;
203
+ /**
204
+ * @param {MessageFormatter[]} formatters - An array of message formatters.
205
+ */
206
+ constructor(formatters) {
207
+ this.#formatters = formatters;
208
+ }
209
+ /**
210
+ * Applies all formatters to the message sequentially.
211
+ * @param {string} message - The message to format.
212
+ * @param {import('types.js').MessageFormatterOptions} options - The options for formatting.
213
+ * @returns {string} The formatted message.
214
+ */
215
+ format(message, options) {
216
+ for (const formatter of this.#formatters) {
217
+ message = formatter.format(message, options);
218
+ }
219
+ return message;
220
+ }
221
+ };
222
+ var PluralMessageFormatter = class {
223
+ /**
224
+ * Formats the message based on the pluralization rules of the given language.
225
+ * @param {string} message - The message to format.
226
+ * @param {import('types.js').MessageFormatterOptions} options - The options for formatting.
227
+ * @returns {string} The formatted message.
228
+ */
229
+ format(message, options) {
230
+ var _a;
231
+ const count = (_a = options.parameters) == null ? void 0 : _a.count;
232
+ if (count === void 0) {
233
+ return message;
234
+ }
235
+ if (!message.includes("|")) {
236
+ if (getNumberOfPluralParts(options.language) > 0) {
237
+ options.report(new Error(`Translator(${options.domain}.${options.key}): Missing plural separator "|" in message.`));
238
+ }
239
+ return message;
240
+ }
241
+ const parts = message.split("|");
242
+ if (parts.length !== getNumberOfPluralParts(options.language)) {
243
+ options.report(new Error(`Translator(${options.domain}.${options.key}): Invalid number of plural parts in message, expected ${getNumberOfPluralParts(options.language)}, ${parts.length} given.`));
244
+ }
245
+ const number = typeof count === "string" ? parseInt(count, 10) : count;
246
+ if (isNaN(number)) {
247
+ options.report(new Error(`Translator(${options.domain}.${options.key}): Invalid count parameter, ${count} given.`));
248
+ return parts[0];
249
+ }
250
+ const partNumber = getLocaleNumber(options.language, number);
251
+ const part = parts[partNumber];
252
+ if (part == null) {
253
+ options.report(new Error(`Translator(${options.domain}.${options.key}): Missing plural part for language "${options.language}" and number ${number}.`));
254
+ return parts[0];
255
+ }
256
+ return part;
257
+ }
258
+ };
259
+ var ParameterMessageFormatter = class {
260
+ /**
261
+ * Formats the message by replacing placeholders with parameter values.
262
+ * @param {string} message - The message to format.
263
+ * @param {import('types.js').MessageFormatterOptions} options - The options for formatting.
264
+ * @returns {string} The formatted message.
265
+ */
266
+ format(message, options) {
267
+ const parameters = options.parameters;
268
+ if (parameters === void 0) {
269
+ return message;
270
+ }
271
+ return message.replace(/\{\s*(.*?)\s*}/g, (_, key) => {
272
+ const val = parameters[key];
273
+ if (val == null) {
274
+ return `{${key}}`;
275
+ }
276
+ return val.toString();
277
+ });
278
+ }
279
+ };
280
+
281
+ // src/index.js
282
+ var Translator = class {
283
+ /**
284
+ * @private
285
+ * @type {Record<string, Record<string, string>> | undefined}
286
+ */
287
+ translations = void 0;
288
+ /**
289
+ * @private
290
+ * @type {Promise<any> | undefined}
291
+ */
292
+ _promise = void 0;
293
+ /**
294
+ * @private
295
+ * @type {string}
296
+ */
297
+ _language;
298
+ /**
299
+ * @private
300
+ * @type {(error: any) => void}
301
+ */
302
+ report;
303
+ /**
304
+ * @private
305
+ * @type {ChainMessageFormatter}
306
+ */
307
+ formatter = new ChainMessageFormatter([
308
+ new PluralMessageFormatter(),
309
+ new ParameterMessageFormatter()
310
+ ]);
311
+ /**
312
+ * @param {import('./types.js').TranslatorOptions} options - Configuration options for the Translator.
313
+ * @throws {Error} If the fallback language is not in the dictionaries.
314
+ */
315
+ constructor(options) {
316
+ this.options = options;
317
+ if (!(this.options.fallbackLanguage in this.options.dictionaries)) {
318
+ throw new Error(`Fallback language "${this.options.fallbackLanguage}" is not available in the dictionaries.`);
319
+ }
320
+ this._language = this.getLanguage(this.options.language);
321
+ this.report = this.options.report || function() {
322
+ };
323
+ this.load(this._language);
324
+ }
325
+ /**
326
+ * Changes the current language and reloads translations.
327
+ * @param {string} val - The new language to switch to.
328
+ * @returns {Promise<void>} Resolves when the language is successfully changed.
329
+ */
330
+ async changeLanguage(val) {
331
+ val = this.getLanguage(val);
332
+ if (this._language !== val) {
333
+ await this.load(val);
334
+ this._language = val;
335
+ }
336
+ }
337
+ /**
338
+ * Gets the current language.
339
+ * @returns {string} The currently set language.
340
+ */
341
+ get language() {
342
+ return this._language;
343
+ }
344
+ /**
345
+ * Waits for any ongoing translation loading to finish.
346
+ * @returns {Promise<void>} Resolves when loading is complete.
347
+ */
348
+ async wait() {
349
+ await this._promise;
350
+ }
351
+ /**
352
+ * Translates a key within a given domain using optional parameters.
353
+ * @param {string} domain - The domain of the translation key.
354
+ * @param {string} key - The key to translate.
355
+ * @param {Record<string, string|number>} [parameters] - Optional parameters for formatting the translation.
356
+ * @returns {string} The translated string, or a fallback if not found.
357
+ */
358
+ trans(domain, key, parameters) {
359
+ var _a;
360
+ if (this.translations === void 0) {
361
+ return "";
362
+ }
363
+ const translation = (_a = this.translations[domain]) == null ? void 0 : _a[key];
364
+ if (translation === void 0) {
365
+ return `${domain}.${key}`;
366
+ }
367
+ return this.formatter.format(translation, {
368
+ language: this._language,
369
+ domain,
370
+ key,
371
+ report: this.report,
372
+ parameters
373
+ });
374
+ }
375
+ /**
376
+ * Loads translations for a specified language.
377
+ * @private
378
+ * @param {string} lang - The language to load.
379
+ * @returns {Promise<void>} Resolves when the translations are loaded.
380
+ */
381
+ async load(lang) {
382
+ const dictionary = this.options.dictionaries[lang];
383
+ if (typeof dictionary === "function") {
384
+ const promise = this._promise = dictionary();
385
+ this.translations = await promise;
386
+ this._promise = void 0;
387
+ } else {
388
+ this.translations = dictionary;
389
+ }
390
+ this.changed();
391
+ }
392
+ /**
393
+ * Determines the appropriate language to use.
394
+ * @private
395
+ * @param {string} lang - The desired language.
396
+ * @returns {string} The language to use, falling back if necessary.
397
+ */
398
+ getLanguage(lang) {
399
+ return lang in this.options.dictionaries ? lang : this.options.fallbackLanguage;
400
+ }
401
+ /**
402
+ * Called when the translations/language have changed. Can be overridden in subclasses.
403
+ * @protected
404
+ */
405
+ changed() {
406
+ }
407
+ };
408
+ export {
409
+ Translator
410
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@svelstack/translator",
3
+ "version": "0.9.0",
4
+ "scripts": {
5
+ "build": "tsup && npm run prepack",
6
+ "prepack": "publint",
7
+ "check": "tsc --noEmit",
8
+ "test": "vitest"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "module": "./dist/index.js",
13
+ "types": "./src/types.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/types.d.ts",
17
+ "import": "./dist/index.js",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src/types.d.ts",
24
+ "*.d.ts",
25
+ "README.md"
26
+ ],
27
+ "devDependencies": {
28
+ "publint": "^0.3.2",
29
+ "tsup": "^8.3.5",
30
+ "vite": "^6.0.7",
31
+ "vitest": "^2.1.8",
32
+ "@sveltejs/vite-plugin-svelte": "^5.0.3"
33
+ }
34
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ type Dictionary = {
2
+ [domain: string]: Record<string, string>;
3
+ };
4
+
5
+ export type TranslatorOptions = {
6
+ language: string;
7
+ fallbackLanguage: string;
8
+ dictionaries: {
9
+ [lang: string]: (() => Promise<Dictionary>) | Dictionary;
10
+ },
11
+ report?: (error: any) => void;
12
+ };
13
+
14
+ export class Translator {
15
+
16
+ constructor(options: TranslatorOptions);
17
+
18
+ changeLanguage(val: string): Promise<void>;
19
+
20
+ get language(): string;
21
+
22
+ /**
23
+ * Wait for the translations to be loaded.
24
+ */
25
+ wait(): Promise<void>;
26
+
27
+ trans(domain: string, key: string, parameters?: Record<string, string|number>): string;
28
+
29
+ protected changed(): void;
30
+ }
31
+
32
+ export interface TypesafeTranslator<Mapping extends Record<string, Record<string, string>>> extends Translator {
33
+
34
+ trans<Domain extends keyof Mapping, Key extends keyof Mapping[Domain]>(
35
+ domain: Domain,
36
+ key: Key,
37
+ ...rest: Mapping[Domain][Key] extends never ? [parameters?: undefined] : [parameters: Record<Mapping[Domain][Key], string>]
38
+ ): string;
39
+
40
+ }
41
+
42
+ export interface MessageFormatter {
43
+
44
+ format(message: string, options: MessageFormatterOptions): string;
45
+
46
+ }
47
+
48
+ export type MessageFormatterOptions = {
49
+ language: string;
50
+ domain: string;
51
+ key: string;
52
+ report: (error: any) => void;
53
+ parameters?: Record<string, string|number>;
54
+ };
55
+
56
+ export class ChainMessageFormatter implements MessageFormatter {
57
+
58
+ constructor(formatters: MessageFormatter[]);
59
+
60
+ format(message: string, options: MessageFormatterOptions): string;
61
+
62
+ }
63
+
64
+ export class PluralMessageFormatter implements MessageFormatter {
65
+
66
+ format(message: string, options: MessageFormatterOptions): string;
67
+
68
+ }
69
+
70
+ export class ParameterMessageFormatter implements MessageFormatter {
71
+
72
+ format(message: string, options: MessageFormatterOptions): string;
73
+
74
+ }