@coherent.js/i18n 1.0.0-beta.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Thomas Drouvin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @coherent.js/i18n
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@coherent.js/i18n.svg)](https://www.npmjs.com/package/@coherent.js/i18n)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
5
+ [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
6
+
7
+ Internationalization utilities for Coherent.js applications.
8
+
9
+ - ESM-only, Node 20+
10
+ - Translator + locale management
11
+ - Date/number/currency/list formatters
12
+
13
+ For a high-level overview and repository-wide instructions, see the root README: ../../README.md
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add @coherent.js/i18n
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ JavaScript (ESM):
24
+ ```js
25
+ import { createTranslator } from '@coherent.js/i18n/translator';
26
+
27
+ const t = createTranslator({
28
+ en: { hello: 'Hello, {name}!' },
29
+ fr: { hello: 'Bonjour, {name} !' }
30
+ }, { locale: 'en' });
31
+
32
+ console.log(t('hello', { name: 'Coherent' }));
33
+ ```
34
+
35
+ TypeScript:
36
+ ```ts
37
+ import { createTranslator } from '@coherent.js/i18n/translator';
38
+
39
+ type Messages = {
40
+ hello: string;
41
+ };
42
+
43
+ const t = createTranslator<{ en: Messages; fr: Messages }>({
44
+ en: { hello: 'Hello, {name}!' },
45
+ fr: { hello: 'Bonjour, {name} !' }
46
+ }, { locale: 'en' });
47
+
48
+ console.log(t('hello', { name: 'TS' }));
49
+ ```
50
+
51
+ ### Formatters and locale
52
+
53
+ ```js
54
+ import { createFormatters } from '@coherent.js/i18n/formatters';
55
+ import { createLocaleManager } from '@coherent.js/i18n/locale';
56
+
57
+ const locale = createLocaleManager('en-US');
58
+ const fmt = createFormatters(locale.current());
59
+
60
+ fmt.date(new Date());
61
+ fmt.number(12345.678);
62
+ fmt.currency(1999.99, 'USD');
63
+ ```
64
+
65
+ ## Exports
66
+
67
+ - `@coherent.js/i18n` (index)
68
+ - `@coherent.js/i18n/translator`
69
+ - `@coherent.js/i18n/formatters`
70
+ - `@coherent.js/i18n/locale`
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ pnpm --filter @coherent.js/i18n run test
76
+ pnpm --filter @coherent.js/i18n run test:watch
77
+ pnpm --filter @coherent.js/i18n run typecheck
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT © Coherent.js Team
package/dist/index.js ADDED
@@ -0,0 +1,725 @@
1
+ // src/translator.js
2
+ var Translator = class {
3
+ constructor(options = {}) {
4
+ this.options = {
5
+ defaultLocale: "en",
6
+ fallbackLocale: "en",
7
+ missingKeyHandler: null,
8
+ interpolation: {
9
+ prefix: "{{",
10
+ suffix: "}}"
11
+ },
12
+ ...options
13
+ };
14
+ this.translations = /* @__PURE__ */ new Map();
15
+ this.currentLocale = this.options.defaultLocale;
16
+ this.loadedLocales = /* @__PURE__ */ new Set();
17
+ }
18
+ /**
19
+ * Add translations for a locale
20
+ *
21
+ * @param {string} locale - Locale code (e.g., 'en', 'fr', 'es')
22
+ * @param {Object} translations - Translation object
23
+ */
24
+ addTranslations(locale, translations) {
25
+ if (!this.translations.has(locale)) {
26
+ this.translations.set(locale, {});
27
+ }
28
+ const existing = this.translations.get(locale);
29
+ this.translations.set(locale, this.deepMerge(existing, translations));
30
+ this.loadedLocales.add(locale);
31
+ }
32
+ /**
33
+ * Deep merge objects
34
+ */
35
+ deepMerge(target, source) {
36
+ const result = { ...target };
37
+ for (const [key, value] of Object.entries(source)) {
38
+ if (value && typeof value === "object" && !Array.isArray(value)) {
39
+ result[key] = this.deepMerge(result[key] || {}, value);
40
+ } else {
41
+ result[key] = value;
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ /**
47
+ * Set current locale
48
+ *
49
+ * @param {string} locale - Locale code
50
+ */
51
+ setLocale(locale) {
52
+ if (!this.loadedLocales.has(locale)) {
53
+ console.warn(`Locale ${locale} not loaded, using fallback`);
54
+ this.currentLocale = this.options.fallbackLocale;
55
+ } else {
56
+ this.currentLocale = locale;
57
+ }
58
+ }
59
+ /**
60
+ * Get current locale
61
+ *
62
+ * @returns {string} Current locale code
63
+ */
64
+ getLocale() {
65
+ return this.currentLocale;
66
+ }
67
+ /**
68
+ * Translate a key
69
+ *
70
+ * @param {string} key - Translation key (supports dot notation)
71
+ * @param {Object} [params] - Interpolation parameters
72
+ * @param {string} [locale] - Override locale
73
+ * @returns {string} Translated string
74
+ */
75
+ t(key, params = {}, locale = null) {
76
+ const targetLocale = locale || this.currentLocale;
77
+ let translation = this.getTranslation(key, targetLocale);
78
+ if (translation === null && targetLocale !== this.options.fallbackLocale) {
79
+ translation = this.getTranslation(key, this.options.fallbackLocale);
80
+ }
81
+ if (translation === null) {
82
+ if (this.options.missingKeyHandler) {
83
+ return this.options.missingKeyHandler(key, targetLocale);
84
+ }
85
+ return key;
86
+ }
87
+ if (typeof translation === "object" && params.count !== void 0) {
88
+ translation = this.selectPlural(translation, params.count, targetLocale);
89
+ }
90
+ if (typeof translation === "string") {
91
+ return this.interpolate(translation, params);
92
+ }
93
+ return String(translation);
94
+ }
95
+ /**
96
+ * Get translation from nested object
97
+ */
98
+ getTranslation(key, locale) {
99
+ const translations = this.translations.get(locale);
100
+ if (!translations) return null;
101
+ const keys = key.split(".");
102
+ let value = translations;
103
+ for (const k of keys) {
104
+ if (value && typeof value === "object" && k in value) {
105
+ value = value[k];
106
+ } else {
107
+ return null;
108
+ }
109
+ }
110
+ return value;
111
+ }
112
+ /**
113
+ * Select plural form
114
+ */
115
+ selectPlural(pluralObject, count, locale) {
116
+ if (count === 0 && pluralObject.zero) {
117
+ return pluralObject.zero;
118
+ }
119
+ if (typeof Intl !== "undefined" && Intl.PluralRules) {
120
+ const rules = new Intl.PluralRules(locale);
121
+ const rule = rules.select(count);
122
+ if (pluralObject[rule]) {
123
+ return pluralObject[rule];
124
+ }
125
+ }
126
+ if (count === 1 && pluralObject.one) {
127
+ return pluralObject.one;
128
+ } else if (pluralObject.other) {
129
+ return pluralObject.other;
130
+ }
131
+ return pluralObject.one || pluralObject.other || "";
132
+ }
133
+ /**
134
+ * Interpolate parameters into string
135
+ */
136
+ interpolate(str, params) {
137
+ const { prefix, suffix } = this.options.interpolation;
138
+ let result = str;
139
+ for (const [key, value] of Object.entries(params)) {
140
+ const placeholder = `${prefix}${key}${suffix}`;
141
+ result = result.replace(new RegExp(placeholder, "g"), String(value));
142
+ }
143
+ return result;
144
+ }
145
+ /**
146
+ * Check if translation exists
147
+ *
148
+ * @param {string} key - Translation key
149
+ * @param {string} [locale] - Locale to check
150
+ * @returns {boolean} True if translation exists
151
+ */
152
+ has(key, locale = null) {
153
+ const targetLocale = locale || this.currentLocale;
154
+ return this.getTranslation(key, targetLocale) !== null;
155
+ }
156
+ /**
157
+ * Get all translations for current locale
158
+ *
159
+ * @returns {Object} All translations
160
+ */
161
+ getTranslations(locale = null) {
162
+ const targetLocale = locale || this.currentLocale;
163
+ return this.translations.get(targetLocale) || {};
164
+ }
165
+ /**
166
+ * Get all loaded locales
167
+ *
168
+ * @returns {Array<string>} Array of locale codes
169
+ */
170
+ getLoadedLocales() {
171
+ return Array.from(this.loadedLocales);
172
+ }
173
+ /**
174
+ * Remove translations for a locale
175
+ *
176
+ * @param {string} locale - Locale code
177
+ */
178
+ removeLocale(locale) {
179
+ this.translations.delete(locale);
180
+ this.loadedLocales.delete(locale);
181
+ if (this.currentLocale === locale) {
182
+ this.currentLocale = this.options.defaultLocale;
183
+ }
184
+ }
185
+ /**
186
+ * Clear all translations
187
+ */
188
+ clear() {
189
+ this.translations.clear();
190
+ this.loadedLocales.clear();
191
+ this.currentLocale = this.options.defaultLocale;
192
+ }
193
+ };
194
+ function createTranslator(options = {}) {
195
+ return new Translator(options);
196
+ }
197
+ function createScopedTranslator(translator, namespace) {
198
+ return {
199
+ t: (key, params, locale) => {
200
+ return translator.t(`${namespace}.${key}`, params, locale);
201
+ },
202
+ has: (key, locale) => {
203
+ return translator.has(`${namespace}.${key}`, locale);
204
+ },
205
+ getLocale: () => translator.getLocale(),
206
+ setLocale: (locale) => translator.setLocale(locale)
207
+ };
208
+ }
209
+
210
+ // src/formatters.js
211
+ var DateFormatter = class {
212
+ constructor(locale = "en") {
213
+ this.locale = locale;
214
+ }
215
+ /**
216
+ * Format a date
217
+ *
218
+ * @param {Date|string|number} date - Date to format
219
+ * @param {Object} [options] - Intl.DateTimeFormat options
220
+ * @returns {string} Formatted date
221
+ */
222
+ format(date, options = {}) {
223
+ const dateObj = date instanceof Date ? date : new Date(date);
224
+ if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
225
+ const formatter = new Intl.DateTimeFormat(this.locale, options);
226
+ return formatter.format(dateObj);
227
+ }
228
+ return dateObj.toLocaleDateString();
229
+ }
230
+ /**
231
+ * Format date as short (e.g., 1/1/2024)
232
+ */
233
+ short(date) {
234
+ return this.format(date, {
235
+ year: "numeric",
236
+ month: "numeric",
237
+ day: "numeric"
238
+ });
239
+ }
240
+ /**
241
+ * Format date as medium (e.g., Jan 1, 2024)
242
+ */
243
+ medium(date) {
244
+ return this.format(date, {
245
+ year: "numeric",
246
+ month: "short",
247
+ day: "numeric"
248
+ });
249
+ }
250
+ /**
251
+ * Format date as long (e.g., January 1, 2024)
252
+ */
253
+ long(date) {
254
+ return this.format(date, {
255
+ year: "numeric",
256
+ month: "long",
257
+ day: "numeric"
258
+ });
259
+ }
260
+ /**
261
+ * Format date as full (e.g., Monday, January 1, 2024)
262
+ */
263
+ full(date) {
264
+ return this.format(date, {
265
+ weekday: "long",
266
+ year: "numeric",
267
+ month: "long",
268
+ day: "numeric"
269
+ });
270
+ }
271
+ /**
272
+ * Format time
273
+ */
274
+ time(date, options = {}) {
275
+ return this.format(date, {
276
+ hour: "numeric",
277
+ minute: "numeric",
278
+ ...options
279
+ });
280
+ }
281
+ /**
282
+ * Format date and time
283
+ */
284
+ dateTime(date, options = {}) {
285
+ return this.format(date, {
286
+ year: "numeric",
287
+ month: "short",
288
+ day: "numeric",
289
+ hour: "numeric",
290
+ minute: "numeric",
291
+ ...options
292
+ });
293
+ }
294
+ /**
295
+ * Format relative time (e.g., "2 days ago")
296
+ */
297
+ relative(date) {
298
+ const dateObj = date instanceof Date ? date : new Date(date);
299
+ const now = /* @__PURE__ */ new Date();
300
+ const diffMs = now - dateObj;
301
+ const diffSec = Math.floor(diffMs / 1e3);
302
+ const diffMin = Math.floor(diffSec / 60);
303
+ const diffHour = Math.floor(diffMin / 60);
304
+ const diffDay = Math.floor(diffHour / 24);
305
+ if (typeof Intl !== "undefined" && Intl.RelativeTimeFormat) {
306
+ const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: "auto" });
307
+ if (diffDay > 0) {
308
+ return rtf.format(-diffDay, "day");
309
+ } else if (diffHour > 0) {
310
+ return rtf.format(-diffHour, "hour");
311
+ } else if (diffMin > 0) {
312
+ return rtf.format(-diffMin, "minute");
313
+ } else {
314
+ return rtf.format(-diffSec, "second");
315
+ }
316
+ }
317
+ if (diffDay > 0) {
318
+ return `${diffDay} day${diffDay > 1 ? "s" : ""} ago`;
319
+ } else if (diffHour > 0) {
320
+ return `${diffHour} hour${diffHour > 1 ? "s" : ""} ago`;
321
+ } else if (diffMin > 0) {
322
+ return `${diffMin} minute${diffMin > 1 ? "s" : ""} ago`;
323
+ } else {
324
+ return "just now";
325
+ }
326
+ }
327
+ };
328
+ var NumberFormatter = class {
329
+ constructor(locale = "en") {
330
+ this.locale = locale;
331
+ }
332
+ /**
333
+ * Format a number
334
+ *
335
+ * @param {number} value - Number to format
336
+ * @param {Object} [options] - Intl.NumberFormat options
337
+ * @returns {string} Formatted number
338
+ */
339
+ format(value, options = {}) {
340
+ if (typeof Intl !== "undefined" && Intl.NumberFormat) {
341
+ const formatter = new Intl.NumberFormat(this.locale, options);
342
+ return formatter.format(value);
343
+ }
344
+ return value.toLocaleString();
345
+ }
346
+ /**
347
+ * Format as decimal
348
+ */
349
+ decimal(value, decimals = 2) {
350
+ return this.format(value, {
351
+ minimumFractionDigits: decimals,
352
+ maximumFractionDigits: decimals
353
+ });
354
+ }
355
+ /**
356
+ * Format as percentage
357
+ */
358
+ percent(value, decimals = 0) {
359
+ return this.format(value, {
360
+ style: "percent",
361
+ minimumFractionDigits: decimals,
362
+ maximumFractionDigits: decimals
363
+ });
364
+ }
365
+ /**
366
+ * Format as compact (e.g., 1.2K, 3.4M)
367
+ */
368
+ compact(value) {
369
+ if (typeof Intl !== "undefined" && Intl.NumberFormat) {
370
+ try {
371
+ const formatter = new Intl.NumberFormat(this.locale, {
372
+ notation: "compact",
373
+ compactDisplay: "short"
374
+ });
375
+ return formatter.format(value);
376
+ } catch {
377
+ }
378
+ }
379
+ if (value >= 1e9) {
380
+ return `${(value / 1e9).toFixed(1)}B`;
381
+ } else if (value >= 1e6) {
382
+ return `${(value / 1e6).toFixed(1)}M`;
383
+ } else if (value >= 1e3) {
384
+ return `${(value / 1e3).toFixed(1)}K`;
385
+ }
386
+ return String(value);
387
+ }
388
+ /**
389
+ * Format with units
390
+ */
391
+ unit(value, unit, options = {}) {
392
+ if (typeof Intl !== "undefined" && Intl.NumberFormat) {
393
+ try {
394
+ const formatter = new Intl.NumberFormat(this.locale, {
395
+ style: "unit",
396
+ unit,
397
+ ...options
398
+ });
399
+ return formatter.format(value);
400
+ } catch {
401
+ }
402
+ }
403
+ return `${value} ${unit}`;
404
+ }
405
+ };
406
+ var CurrencyFormatter = class {
407
+ constructor(locale = "en", defaultCurrency = "USD") {
408
+ this.locale = locale;
409
+ this.defaultCurrency = defaultCurrency;
410
+ }
411
+ /**
412
+ * Format a currency value
413
+ *
414
+ * @param {number} value - Amount to format
415
+ * @param {string} [currency] - Currency code (e.g., 'USD', 'EUR')
416
+ * @param {Object} [options] - Additional options
417
+ * @returns {string} Formatted currency
418
+ */
419
+ format(value, currency = null, options = {}) {
420
+ const currencyCode = currency || this.defaultCurrency;
421
+ if (typeof Intl !== "undefined" && Intl.NumberFormat) {
422
+ const formatter = new Intl.NumberFormat(this.locale, {
423
+ style: "currency",
424
+ currency: currencyCode,
425
+ ...options
426
+ });
427
+ return formatter.format(value);
428
+ }
429
+ return `${currencyCode} ${value.toFixed(2)}`;
430
+ }
431
+ /**
432
+ * Format without decimal places
433
+ */
434
+ whole(value, currency = null) {
435
+ return this.format(value, currency, {
436
+ minimumFractionDigits: 0,
437
+ maximumFractionDigits: 0
438
+ });
439
+ }
440
+ /**
441
+ * Format with symbol only (no code)
442
+ */
443
+ symbol(value, currency = null) {
444
+ return this.format(value, currency, {
445
+ currencyDisplay: "symbol"
446
+ });
447
+ }
448
+ /**
449
+ * Format with narrow symbol
450
+ */
451
+ narrowSymbol(value, currency = null) {
452
+ return this.format(value, currency, {
453
+ currencyDisplay: "narrowSymbol"
454
+ });
455
+ }
456
+ /**
457
+ * Format with code (e.g., USD 100.00)
458
+ */
459
+ code(value, currency = null) {
460
+ return this.format(value, currency, {
461
+ currencyDisplay: "code"
462
+ });
463
+ }
464
+ };
465
+ var ListFormatter = class {
466
+ constructor(locale = "en") {
467
+ this.locale = locale;
468
+ }
469
+ /**
470
+ * Format a list
471
+ *
472
+ * @param {Array} items - Items to format
473
+ * @param {Object} [options] - Formatting options
474
+ * @returns {string} Formatted list
475
+ */
476
+ format(items, options = {}) {
477
+ if (typeof Intl !== "undefined" && Intl.ListFormat) {
478
+ const formatter = new Intl.ListFormat(this.locale, options);
479
+ return formatter.format(items);
480
+ }
481
+ if (items.length === 0) return "";
482
+ if (items.length === 1) return items[0];
483
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
484
+ const last = items[items.length - 1];
485
+ const rest = items.slice(0, -1);
486
+ return `${rest.join(", ")}, and ${last}`;
487
+ }
488
+ /**
489
+ * Format as conjunction (and)
490
+ */
491
+ and(items) {
492
+ return this.format(items, { type: "conjunction" });
493
+ }
494
+ /**
495
+ * Format as disjunction (or)
496
+ */
497
+ or(items) {
498
+ return this.format(items, { type: "disjunction" });
499
+ }
500
+ /**
501
+ * Format as unit list
502
+ */
503
+ unit(items) {
504
+ return this.format(items, { type: "unit" });
505
+ }
506
+ };
507
+ function createFormatters(locale = "en", options = {}) {
508
+ return {
509
+ date: new DateFormatter(locale),
510
+ number: new NumberFormatter(locale),
511
+ currency: new CurrencyFormatter(locale, options.defaultCurrency),
512
+ list: new ListFormatter(locale)
513
+ };
514
+ }
515
+
516
+ // src/locale.js
517
+ function detectLocale() {
518
+ if (typeof navigator !== "undefined") {
519
+ if (navigator.language) {
520
+ return normalizeLocale(navigator.language);
521
+ }
522
+ if (navigator.languages && navigator.languages.length > 0) {
523
+ return normalizeLocale(navigator.languages[0]);
524
+ }
525
+ if (navigator.userLanguage) {
526
+ return normalizeLocale(navigator.userLanguage);
527
+ }
528
+ }
529
+ return "en";
530
+ }
531
+ function normalizeLocale(locale, keepRegion = false) {
532
+ if (!locale) return "en";
533
+ let normalized = locale.toLowerCase().replace("_", "-");
534
+ if (!keepRegion && normalized.includes("-")) {
535
+ normalized = normalized.split("-")[0];
536
+ }
537
+ return normalized;
538
+ }
539
+ function parseLocale(locale) {
540
+ const normalized = locale.replace("_", "-");
541
+ const parts = normalized.split("-");
542
+ return {
543
+ language: parts[0]?.toLowerCase() || "en",
544
+ region: parts[1]?.toUpperCase() || null,
545
+ script: parts.length > 2 ? parts[1] : null,
546
+ full: normalized
547
+ };
548
+ }
549
+ function getLocaleDirection(locale) {
550
+ const rtlLocales = ["ar", "he", "fa", "ur", "yi"];
551
+ const language = parseLocale(locale).language;
552
+ return rtlLocales.includes(language) ? "rtl" : "ltr";
553
+ }
554
+ function isRTL(locale) {
555
+ return getLocaleDirection(locale) === "rtl";
556
+ }
557
+ function getLocaleDisplayName(locale, displayLocale = "en") {
558
+ if (typeof Intl !== "undefined" && Intl.DisplayNames) {
559
+ try {
560
+ const displayNames = new Intl.DisplayNames([displayLocale], { type: "language" });
561
+ return displayNames.of(locale);
562
+ } catch {
563
+ }
564
+ }
565
+ return locale;
566
+ }
567
+ function matchLocale(requestedLocale, availableLocales, defaultLocale = "en") {
568
+ const normalized = normalizeLocale(requestedLocale);
569
+ if (availableLocales.includes(normalized)) {
570
+ return normalized;
571
+ }
572
+ const withRegion = normalizeLocale(requestedLocale, true);
573
+ if (availableLocales.includes(withRegion)) {
574
+ return withRegion;
575
+ }
576
+ const language = parseLocale(requestedLocale).language;
577
+ const languageMatch = availableLocales.find(
578
+ (locale) => parseLocale(locale).language === language
579
+ );
580
+ if (languageMatch) {
581
+ return languageMatch;
582
+ }
583
+ return availableLocales.includes(defaultLocale) ? defaultLocale : availableLocales[0];
584
+ }
585
+ function getSupportedLocales() {
586
+ if (typeof navigator !== "undefined" && navigator.languages) {
587
+ return navigator.languages.map((locale) => normalizeLocale(locale));
588
+ }
589
+ return [detectLocale()];
590
+ }
591
+ var LocaleManager = class {
592
+ constructor(options = {}) {
593
+ this.options = {
594
+ defaultLocale: "en",
595
+ availableLocales: ["en"],
596
+ storageKey: "coherent-locale",
597
+ autoDetect: true,
598
+ ...options
599
+ };
600
+ this.currentLocale = this.options.defaultLocale;
601
+ this.listeners = [];
602
+ if (this.options.autoDetect) {
603
+ this.currentLocale = this.detectAndMatch();
604
+ }
605
+ this.loadFromStorage();
606
+ }
607
+ /**
608
+ * Detect and match best locale
609
+ */
610
+ detectAndMatch() {
611
+ const detected = detectLocale();
612
+ return matchLocale(
613
+ detected,
614
+ this.options.availableLocales,
615
+ this.options.defaultLocale
616
+ );
617
+ }
618
+ /**
619
+ * Get current locale
620
+ */
621
+ getLocale() {
622
+ return this.currentLocale;
623
+ }
624
+ /**
625
+ * Set locale
626
+ */
627
+ setLocale(locale) {
628
+ const matched = matchLocale(
629
+ locale,
630
+ this.options.availableLocales,
631
+ this.options.defaultLocale
632
+ );
633
+ if (matched !== this.currentLocale) {
634
+ const oldLocale = this.currentLocale;
635
+ this.currentLocale = matched;
636
+ this.saveToStorage();
637
+ this.notifyListeners(oldLocale, matched);
638
+ }
639
+ }
640
+ /**
641
+ * Add locale change listener
642
+ */
643
+ onChange(listener) {
644
+ this.listeners.push(listener);
645
+ return () => {
646
+ const index = this.listeners.indexOf(listener);
647
+ if (index > -1) {
648
+ this.listeners.splice(index, 1);
649
+ }
650
+ };
651
+ }
652
+ /**
653
+ * Notify listeners of locale change
654
+ */
655
+ notifyListeners(oldLocale, newLocale) {
656
+ this.listeners.forEach((listener) => {
657
+ try {
658
+ listener(newLocale, oldLocale);
659
+ } catch (error) {
660
+ console.error("Error in locale change listener:", error);
661
+ }
662
+ });
663
+ }
664
+ /**
665
+ * Save locale to storage
666
+ */
667
+ saveToStorage() {
668
+ if (typeof localStorage !== "undefined") {
669
+ try {
670
+ localStorage.setItem(this.options.storageKey, this.currentLocale);
671
+ } catch {
672
+ }
673
+ }
674
+ }
675
+ /**
676
+ * Load locale from storage
677
+ */
678
+ loadFromStorage() {
679
+ if (typeof localStorage !== "undefined") {
680
+ try {
681
+ const stored = localStorage.getItem(this.options.storageKey);
682
+ if (stored) {
683
+ this.setLocale(stored);
684
+ }
685
+ } catch {
686
+ }
687
+ }
688
+ }
689
+ /**
690
+ * Get available locales
691
+ */
692
+ getAvailableLocales() {
693
+ return [...this.options.availableLocales];
694
+ }
695
+ /**
696
+ * Check if locale is available
697
+ */
698
+ isAvailable(locale) {
699
+ return this.options.availableLocales.includes(locale);
700
+ }
701
+ };
702
+ function createLocaleManager(options = {}) {
703
+ return new LocaleManager(options);
704
+ }
705
+ export {
706
+ CurrencyFormatter,
707
+ DateFormatter,
708
+ ListFormatter,
709
+ LocaleManager,
710
+ NumberFormatter,
711
+ Translator,
712
+ createFormatters,
713
+ createLocaleManager,
714
+ createScopedTranslator,
715
+ createTranslator,
716
+ detectLocale,
717
+ getLocaleDirection,
718
+ getLocaleDisplayName,
719
+ getSupportedLocales,
720
+ isRTL,
721
+ matchLocale,
722
+ normalizeLocale,
723
+ parseLocale
724
+ };
725
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@coherent.js/i18n",
3
+ "version": "1.0.0-beta.2",
4
+ "description": "Internationalization support for Coherent.js applications",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./translator": "./dist/translator.js",
10
+ "./formatters": "./dist/formatters.js",
11
+ "./locale": "./dist/locale.js"
12
+ },
13
+ "keywords": [
14
+ "coherent",
15
+ "i18n",
16
+ "internationalization",
17
+ "localization",
18
+ "translation"
19
+ ],
20
+ "author": "Coherent.js Team",
21
+ "license": "MIT",
22
+ "peerDependencies": {
23
+ "@coherent.js/core": "1.0.0-beta.2"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Tomdrouv1/coherent.js.git"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "types": "./types/index.d.ts",
33
+ "files": [
34
+ "LICENSE",
35
+ "README.md",
36
+ "types/"
37
+ ],
38
+ "scripts": {
39
+ "build": "node build.mjs",
40
+ "clean": "rm -rf dist"
41
+ }
42
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Coherent.js I18n TypeScript Definitions
3
+ * @module @coherent.js/i18n
4
+ */
5
+
6
+ // ===== Translator Types =====
7
+
8
+ export interface TranslationMessages {
9
+ [key: string]: string | TranslationMessages;
10
+ }
11
+
12
+ export interface TranslatorOptions {
13
+ locale: string;
14
+ messages: TranslationMessages;
15
+ fallbackLocale?: string;
16
+ fallbackMessages?: TranslationMessages;
17
+ interpolation?: {
18
+ prefix?: string;
19
+ suffix?: string;
20
+ };
21
+ pluralization?: boolean;
22
+ contextSeparator?: string;
23
+ }
24
+
25
+ export class Translator {
26
+ constructor(options: TranslatorOptions);
27
+ t(key: string, params?: Record<string, any>): string;
28
+ translate(key: string, params?: Record<string, any>): string;
29
+ has(key: string): boolean;
30
+ setLocale(locale: string): void;
31
+ getLocale(): string;
32
+ addMessages(messages: TranslationMessages, locale?: string): void;
33
+ removeMessages(keys: string[], locale?: string): void;
34
+ }
35
+
36
+ export function createTranslator(options: TranslatorOptions): Translator;
37
+ export function createScopedTranslator(translator: Translator, scope: string): Translator;
38
+
39
+ // ===== Formatters Types =====
40
+
41
+ export interface DateFormatterOptions {
42
+ locale?: string;
43
+ timeZone?: string;
44
+ dateStyle?: 'full' | 'long' | 'medium' | 'short';
45
+ timeStyle?: 'full' | 'long' | 'medium' | 'short';
46
+ format?: string;
47
+ }
48
+
49
+ export class DateFormatter {
50
+ constructor(locale?: string, options?: DateFormatterOptions);
51
+ format(date: Date | number | string): string;
52
+ formatRelative(date: Date | number | string): string;
53
+ formatDistance(date: Date | number | string, baseDate?: Date | number): string;
54
+ }
55
+
56
+ export interface NumberFormatterOptions {
57
+ locale?: string;
58
+ style?: 'decimal' | 'currency' | 'percent' | 'unit';
59
+ currency?: string;
60
+ minimumFractionDigits?: number;
61
+ maximumFractionDigits?: number;
62
+ useGrouping?: boolean;
63
+ }
64
+
65
+ export class NumberFormatter {
66
+ constructor(locale?: string, options?: NumberFormatterOptions);
67
+ format(value: number): string;
68
+ formatCompact(value: number): string;
69
+ formatPercent(value: number): string;
70
+ }
71
+
72
+ export interface CurrencyFormatterOptions {
73
+ locale?: string;
74
+ currency: string;
75
+ display?: 'symbol' | 'code' | 'name';
76
+ }
77
+
78
+ export class CurrencyFormatter {
79
+ constructor(locale?: string, options?: CurrencyFormatterOptions);
80
+ format(value: number): string;
81
+ }
82
+
83
+ export interface ListFormatterOptions {
84
+ locale?: string;
85
+ type?: 'conjunction' | 'disjunction' | 'unit';
86
+ style?: 'long' | 'short' | 'narrow';
87
+ }
88
+
89
+ export class ListFormatter {
90
+ constructor(locale?: string, options?: ListFormatterOptions);
91
+ format(list: string[]): string;
92
+ }
93
+
94
+ export interface Formatters {
95
+ date: DateFormatter;
96
+ number: NumberFormatter;
97
+ currency: CurrencyFormatter;
98
+ list: ListFormatter;
99
+ }
100
+
101
+ export function createFormatters(locale: string): Formatters;
102
+
103
+ // ===== Locale Manager Types =====
104
+
105
+ export interface LocaleConfig {
106
+ code: string;
107
+ name: string;
108
+ nativeName: string;
109
+ direction?: 'ltr' | 'rtl';
110
+ dateFormat?: string;
111
+ timeFormat?: string;
112
+ firstDayOfWeek?: number;
113
+ }
114
+
115
+ export class LocaleManager {
116
+ constructor(locales: LocaleConfig[]);
117
+ addLocale(locale: LocaleConfig): void;
118
+ removeLocale(code: string): void;
119
+ getLocale(code: string): LocaleConfig | undefined;
120
+ getAllLocales(): LocaleConfig[];
121
+ setCurrentLocale(code: string): void;
122
+ getCurrentLocale(): LocaleConfig;
123
+ }
124
+
125
+ export function createLocaleManager(locales: LocaleConfig[]): LocaleManager;
126
+ export function detectLocale(): string;
127
+ export function normalizeLocale(locale: string): string;
128
+ export function isRTL(locale: string): boolean;