@eouia/intl-msg 0.1.0 → 0.1.1
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/README.md +525 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
Native `Intl`-based i18n message formatting for modern Node.js, browsers, and Electron, with no runtime dependencies.
|
|
4
4
|
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
`intl-msg` started from practical localization problems that show up once a project grows beyond a simple translation table.
|
|
8
|
+
|
|
9
|
+
Typical pain points look like this:
|
|
10
|
+
|
|
11
|
+
- translation files become expensive to maintain because every regional or user-specific variant turns into a full copy
|
|
12
|
+
- locale-specific differences in spelling, grammar, date formatting, and number formatting leak into application code
|
|
13
|
+
- fallback behavior is too simple for real users who may prefer chains such as `fr-CA -> fr -> en-CA -> en`
|
|
14
|
+
- user overrides and minor-locale customizations are awkward to support cleanly
|
|
15
|
+
|
|
16
|
+
The goal of this project is to keep localization lightweight while still supporting:
|
|
17
|
+
|
|
18
|
+
- partial dictionaries instead of full duplicated dictionary snapshots
|
|
19
|
+
- locale-aware formatting driven by native `Intl`
|
|
20
|
+
- explicit locale fallback chains
|
|
21
|
+
- custom formatter behavior that can live near translation data instead of in app logic
|
|
22
|
+
- user or language-pack overrides without forcing the main application to ship every variation
|
|
23
|
+
|
|
24
|
+
In short, `intl-msg` is not just a string lookup helper. It is a small dictionary resolution and message formatting layer built around native `Intl`.
|
|
25
|
+
|
|
26
|
+
For the longer project rationale, see [VISION.md](/Users/seongnohyi/Workspace/work/intl-msg/VISION.md).
|
|
27
|
+
|
|
5
28
|
## Status
|
|
6
29
|
|
|
7
30
|
The package now builds from a single source file and publishes both CommonJS and ESM outputs:
|
|
@@ -223,6 +246,22 @@ The library includes these built-in formatters:
|
|
|
223
246
|
- `duration`
|
|
224
247
|
- `humanizedRelativeTime`
|
|
225
248
|
|
|
249
|
+
### Formatter overview
|
|
250
|
+
|
|
251
|
+
| Formatter | Input shape | Primary purpose |
|
|
252
|
+
| --- | --- | --- |
|
|
253
|
+
| `select` | scalar | Pick a string from `options` by value |
|
|
254
|
+
| `pluralRules` | number | Pick a string from `rules` by plural category |
|
|
255
|
+
| `pluralRange` | `{ start, end }` | Pick a string from `rules` by plural range category |
|
|
256
|
+
| `list` | array | Render a natural-language list |
|
|
257
|
+
| `number` | number | Locale-aware number formatting |
|
|
258
|
+
| `numberRange` | `{ start, end }` | Locale-aware numeric range formatting |
|
|
259
|
+
| `dateTime` | date-like value | Locale-aware date/time formatting |
|
|
260
|
+
| `dateTimeRange` | `{ start, end }` | Locale-aware date/time range formatting |
|
|
261
|
+
| `relativeTime` | number | Relative time phrase with an explicit unit |
|
|
262
|
+
| `humanizedRelativeTime` | date-like value | Relative time phrase with an inferred unit |
|
|
263
|
+
| `duration` | duration record | Duration formatting via `Intl.DurationFormat` |
|
|
264
|
+
|
|
226
265
|
### Example
|
|
227
266
|
|
|
228
267
|
```js
|
|
@@ -340,6 +379,378 @@ msg.message('LABEL', {
|
|
|
340
379
|
|
|
341
380
|
The `pluralRange` formatter expects `{ start, end }` and uses `Intl.PluralRules.prototype.selectRange()`.
|
|
342
381
|
|
|
382
|
+
## Formatter reference
|
|
383
|
+
|
|
384
|
+
### `select`
|
|
385
|
+
|
|
386
|
+
Use `select` when the input value should directly choose a phrase from `options`.
|
|
387
|
+
|
|
388
|
+
```js
|
|
389
|
+
msg.addDictionary({
|
|
390
|
+
en: {
|
|
391
|
+
translations: {
|
|
392
|
+
WELCOME: 'Welcome {{gender:title}} {{name}}.',
|
|
393
|
+
},
|
|
394
|
+
formatters: {
|
|
395
|
+
title: {
|
|
396
|
+
format: 'select',
|
|
397
|
+
options: {
|
|
398
|
+
female: 'Ms.',
|
|
399
|
+
male: 'Mr.',
|
|
400
|
+
other: '',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
msg.message('WELCOME', { gender: 'female', name: 'Taylor' })
|
|
408
|
+
// => 'Welcome Ms. Taylor.'
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Expected input:
|
|
412
|
+
|
|
413
|
+
- any scalar value that can be matched against keys in `options`
|
|
414
|
+
|
|
415
|
+
Fallback behavior:
|
|
416
|
+
|
|
417
|
+
- returns `options.other` when present
|
|
418
|
+
- otherwise falls back to the original input value
|
|
419
|
+
|
|
420
|
+
### `pluralRules`
|
|
421
|
+
|
|
422
|
+
Use `pluralRules` when a number should choose a localized term by plural category.
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
msg.addDictionary({
|
|
426
|
+
en: {
|
|
427
|
+
translations: {
|
|
428
|
+
LABEL: 'There {{count:beVerb}} {{count}} {{count:unit}}.',
|
|
429
|
+
},
|
|
430
|
+
formatters: {
|
|
431
|
+
beVerb: {
|
|
432
|
+
format: 'pluralRules',
|
|
433
|
+
rules: {
|
|
434
|
+
one: 'is',
|
|
435
|
+
other: 'are',
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
unit: {
|
|
439
|
+
format: 'pluralRules',
|
|
440
|
+
rules: {
|
|
441
|
+
one: 'item',
|
|
442
|
+
other: 'items',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
msg.message('LABEL', { count: 2 })
|
|
450
|
+
// => 'There are 2 items.'
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Expected input:
|
|
454
|
+
|
|
455
|
+
- a number
|
|
456
|
+
|
|
457
|
+
Fallback behavior:
|
|
458
|
+
|
|
459
|
+
- returns `rules.other` when available
|
|
460
|
+
- otherwise returns an empty string
|
|
461
|
+
|
|
462
|
+
### `pluralRange`
|
|
463
|
+
|
|
464
|
+
Use `pluralRange` when a numeric range should choose a localized term by range category.
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
msg.addDictionary({
|
|
468
|
+
en: {
|
|
469
|
+
translations: {
|
|
470
|
+
RANGE: '{{countText}} {{count:ticketLabel}}',
|
|
471
|
+
},
|
|
472
|
+
formatters: {
|
|
473
|
+
ticketLabel: {
|
|
474
|
+
format: 'pluralRange',
|
|
475
|
+
rules: {
|
|
476
|
+
one: 'ticket',
|
|
477
|
+
other: 'tickets',
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
msg.message('RANGE', {
|
|
485
|
+
countText: '1-3',
|
|
486
|
+
count: { start: 1, end: 3 },
|
|
487
|
+
})
|
|
488
|
+
// => '1-3 tickets'
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Expected input:
|
|
492
|
+
|
|
493
|
+
- `{ start, end }`
|
|
494
|
+
|
|
495
|
+
Fallback behavior:
|
|
496
|
+
|
|
497
|
+
- warns and falls back when `Intl.PluralRules.prototype.selectRange()` is unavailable
|
|
498
|
+
|
|
499
|
+
### `list`
|
|
500
|
+
|
|
501
|
+
Use `list` when you want locale-aware conjunctions such as "A, B, and C".
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
msg.addDictionary({
|
|
505
|
+
en: {
|
|
506
|
+
translations: {
|
|
507
|
+
COLORS: 'Colors: {{value:palette}}',
|
|
508
|
+
},
|
|
509
|
+
formatters: {
|
|
510
|
+
palette: {
|
|
511
|
+
format: 'list',
|
|
512
|
+
options: { style: 'long', type: 'conjunction' },
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
msg.message('COLORS', { value: ['Red', 'Blue', 'White'] })
|
|
519
|
+
// => 'Colors: Red, Blue, and White'
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Expected input:
|
|
523
|
+
|
|
524
|
+
- an array
|
|
525
|
+
|
|
526
|
+
Fallback behavior:
|
|
527
|
+
|
|
528
|
+
- returns the original value when the input is not an array
|
|
529
|
+
|
|
530
|
+
### `number`
|
|
531
|
+
|
|
532
|
+
Use `number` for locale-aware numbers, currency, percent, or unit display.
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
msg.addDictionary({
|
|
536
|
+
en: {
|
|
537
|
+
translations: {
|
|
538
|
+
TOTAL: 'Total: {{amount:price}}',
|
|
539
|
+
},
|
|
540
|
+
formatters: {
|
|
541
|
+
price: {
|
|
542
|
+
format: 'number',
|
|
543
|
+
options: { style: 'currency', currency: 'USD' },
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
msg.message('TOTAL', { amount: 1234.5 })
|
|
550
|
+
// => 'Total: $1,234.50'
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Expected input:
|
|
554
|
+
|
|
555
|
+
- a number
|
|
556
|
+
|
|
557
|
+
Fallback behavior:
|
|
558
|
+
|
|
559
|
+
- validates commonly used Intl options when supported
|
|
560
|
+
- returns the original value when the input is not numeric or options are invalid
|
|
561
|
+
|
|
562
|
+
### `numberRange`
|
|
563
|
+
|
|
564
|
+
Use `numberRange` when two numeric endpoints should be formatted as one localized range.
|
|
565
|
+
|
|
566
|
+
```js
|
|
567
|
+
msg.addDictionary({
|
|
568
|
+
en: {
|
|
569
|
+
translations: {
|
|
570
|
+
BUDGET: 'Budget: {{amount:budget}}',
|
|
571
|
+
},
|
|
572
|
+
formatters: {
|
|
573
|
+
budget: {
|
|
574
|
+
format: 'numberRange',
|
|
575
|
+
options: { style: 'currency', currency: 'USD' },
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
msg.message('BUDGET', {
|
|
582
|
+
amount: { start: 1200, end: 3400 },
|
|
583
|
+
})
|
|
584
|
+
// => 'Budget: $1,200.00 - $3,400.00'
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Expected input:
|
|
588
|
+
|
|
589
|
+
- `{ start, end }`
|
|
590
|
+
|
|
591
|
+
Fallback behavior:
|
|
592
|
+
|
|
593
|
+
- warns and falls back to two separately formatted values joined by ` - ` when `formatRange()` is unavailable
|
|
594
|
+
|
|
595
|
+
### `dateTime`
|
|
596
|
+
|
|
597
|
+
Use `dateTime` for locale-aware date or time rendering from a date-like input.
|
|
598
|
+
|
|
599
|
+
```js
|
|
600
|
+
msg.addDictionary({
|
|
601
|
+
en: {
|
|
602
|
+
translations: {
|
|
603
|
+
TODAY: 'Today is {{value:dateLabel}}.',
|
|
604
|
+
},
|
|
605
|
+
formatters: {
|
|
606
|
+
dateLabel: {
|
|
607
|
+
format: 'dateTime',
|
|
608
|
+
options: { weekday: 'long', month: 'long', day: 'numeric' },
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
msg.message('TODAY', { value: '2026-03-23' })
|
|
615
|
+
// => 'Today is Monday, March 23.'
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Expected input:
|
|
619
|
+
|
|
620
|
+
- a `Date`, timestamp, or date-like string accepted by `new Date(...)`
|
|
621
|
+
|
|
622
|
+
Fallback behavior:
|
|
623
|
+
|
|
624
|
+
- returns the original value when the input cannot be parsed as a valid date
|
|
625
|
+
|
|
626
|
+
### `dateTimeRange`
|
|
627
|
+
|
|
628
|
+
Use `dateTimeRange` when two date-like values should be rendered as one localized range.
|
|
629
|
+
|
|
630
|
+
```js
|
|
631
|
+
msg.addDictionary({
|
|
632
|
+
en: {
|
|
633
|
+
translations: {
|
|
634
|
+
EVENT: 'Event: {{period:schedule}}',
|
|
635
|
+
},
|
|
636
|
+
formatters: {
|
|
637
|
+
schedule: {
|
|
638
|
+
format: 'dateTimeRange',
|
|
639
|
+
options: { month: 'short', day: 'numeric' },
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
msg.message('EVENT', {
|
|
646
|
+
period: { start: '2026-03-23', end: '2026-03-25' },
|
|
647
|
+
})
|
|
648
|
+
// => 'Event: Mar 23-25'
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Expected input:
|
|
652
|
+
|
|
653
|
+
- `{ start, end }` with date-like values
|
|
654
|
+
|
|
655
|
+
Fallback behavior:
|
|
656
|
+
|
|
657
|
+
- warns and falls back to two separately formatted dates joined by ` - ` when `formatRange()` is unavailable
|
|
658
|
+
|
|
659
|
+
### `relativeTime`
|
|
660
|
+
|
|
661
|
+
Use `relativeTime` when the unit is known in advance.
|
|
662
|
+
|
|
663
|
+
```js
|
|
664
|
+
msg.addDictionary({
|
|
665
|
+
en: {
|
|
666
|
+
translations: {
|
|
667
|
+
ETA: 'ETA: {{value:eta}}',
|
|
668
|
+
},
|
|
669
|
+
formatters: {
|
|
670
|
+
eta: {
|
|
671
|
+
format: 'relativeTime',
|
|
672
|
+
unit: 'day',
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
msg.message('ETA', { value: 3 })
|
|
679
|
+
// => 'ETA: in 3 days'
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Expected input:
|
|
683
|
+
|
|
684
|
+
- a number
|
|
685
|
+
|
|
686
|
+
Fallback behavior:
|
|
687
|
+
|
|
688
|
+
- validates the unit when supported
|
|
689
|
+
- returns the original value when the input is not numeric or the unit is invalid
|
|
690
|
+
|
|
691
|
+
### `humanizedRelativeTime`
|
|
692
|
+
|
|
693
|
+
Use `humanizedRelativeTime` when you want the unit inferred from the distance to now.
|
|
694
|
+
|
|
695
|
+
```js
|
|
696
|
+
msg.addDictionary({
|
|
697
|
+
en: {
|
|
698
|
+
translations: {
|
|
699
|
+
WHEN: 'Updated {{value:ago}}',
|
|
700
|
+
},
|
|
701
|
+
formatters: {
|
|
702
|
+
ago: {
|
|
703
|
+
format: 'humanizedRelativeTime',
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
msg.message('WHEN', { value: new Date(Date.now() - 2 * 60 * 60 * 1000) })
|
|
710
|
+
// => 'Updated 2 hours ago'
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Expected input:
|
|
714
|
+
|
|
715
|
+
- a date-like value
|
|
716
|
+
|
|
717
|
+
Fallback behavior:
|
|
718
|
+
|
|
719
|
+
- returns the original value when the input cannot be parsed as a valid date
|
|
720
|
+
|
|
721
|
+
### `duration`
|
|
722
|
+
|
|
723
|
+
Use `duration` for explicit duration records such as hours, minutes, and seconds.
|
|
724
|
+
|
|
725
|
+
```js
|
|
726
|
+
msg.addDictionary({
|
|
727
|
+
en: {
|
|
728
|
+
translations: {
|
|
729
|
+
ELAPSED: 'Elapsed: {{value:elapsed}}',
|
|
730
|
+
},
|
|
731
|
+
formatters: {
|
|
732
|
+
elapsed: {
|
|
733
|
+
format: 'duration',
|
|
734
|
+
options: { style: 'short' },
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
msg.message('ELAPSED', {
|
|
741
|
+
value: { hours: 1, minutes: 30, seconds: 5 },
|
|
742
|
+
})
|
|
743
|
+
// => 'Elapsed: 1 hr, 30 min, 5 sec'
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Expected input:
|
|
747
|
+
|
|
748
|
+
- an `Intl.DurationFormat`-style duration record object
|
|
749
|
+
|
|
750
|
+
Fallback behavior:
|
|
751
|
+
|
|
752
|
+
- warns and falls back gracefully when `Intl.DurationFormat` is unavailable
|
|
753
|
+
|
|
343
754
|
## Option validation
|
|
344
755
|
|
|
345
756
|
When the runtime supports `Intl.supportedValuesOf()`, the library validates commonly used Intl options before constructing formatters.
|
|
@@ -449,6 +860,12 @@ Supported options:
|
|
|
449
860
|
- `verbose`
|
|
450
861
|
- `intlPolyfill`
|
|
451
862
|
|
|
863
|
+
Example:
|
|
864
|
+
|
|
865
|
+
```js
|
|
866
|
+
const msg = new IntlMsg({ verbose: false })
|
|
867
|
+
```
|
|
868
|
+
|
|
452
869
|
### `IntlMsg.factory(options?)`
|
|
453
870
|
|
|
454
871
|
Convenience constructor. In addition to the constructor options, it also accepts:
|
|
@@ -456,14 +873,40 @@ Convenience constructor. In addition to the constructor options, it also accepts
|
|
|
456
873
|
- `locales`
|
|
457
874
|
- `dictionaries`
|
|
458
875
|
|
|
876
|
+
Example:
|
|
877
|
+
|
|
878
|
+
```js
|
|
879
|
+
const msg = IntlMsg.factory({
|
|
880
|
+
locales: ['en-US', 'en'],
|
|
881
|
+
dictionaries: {
|
|
882
|
+
en: { translations: { HELLO: 'Hello' } },
|
|
883
|
+
},
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
msg.message('HELLO')
|
|
887
|
+
// => 'Hello'
|
|
888
|
+
```
|
|
889
|
+
|
|
459
890
|
### `addLocale(locales)`
|
|
460
891
|
|
|
461
892
|
Adds one locale or an array of locales.
|
|
462
893
|
|
|
894
|
+
```js
|
|
895
|
+
msg.addLocale(['en-US', 'de'])
|
|
896
|
+
msg.getLocale()
|
|
897
|
+
// => ['en-US', 'de']
|
|
898
|
+
```
|
|
899
|
+
|
|
463
900
|
### `setLocale(locales)`
|
|
464
901
|
|
|
465
902
|
Replaces the current locale list.
|
|
466
903
|
|
|
904
|
+
```js
|
|
905
|
+
msg.setLocale(['fr-CA', 'fr', 'en'])
|
|
906
|
+
msg.getLocale()
|
|
907
|
+
// => ['fr-CA', 'fr', 'en']
|
|
908
|
+
```
|
|
909
|
+
|
|
467
910
|
### `getLocale()`
|
|
468
911
|
|
|
469
912
|
Returns the current locale list.
|
|
@@ -472,34 +915,85 @@ Returns the current locale list.
|
|
|
472
915
|
|
|
473
916
|
Merges dictionary data into the current instance.
|
|
474
917
|
|
|
918
|
+
```js
|
|
919
|
+
msg.addDictionary({
|
|
920
|
+
en: {
|
|
921
|
+
translations: {
|
|
922
|
+
HELLO: 'Hello, {{name}}.',
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
msg.message('HELLO', { name: 'Taylor' })
|
|
928
|
+
// => 'Hello, Taylor.'
|
|
929
|
+
```
|
|
930
|
+
|
|
475
931
|
### `getDictionary(locale)`
|
|
476
932
|
|
|
477
933
|
Returns the `Dictionary` instance for a locale, or `null`.
|
|
478
934
|
|
|
935
|
+
```js
|
|
936
|
+
msg.getDictionary('en')
|
|
937
|
+
// => Dictionary instance or null
|
|
938
|
+
```
|
|
939
|
+
|
|
479
940
|
### `getDictionaryNames()`
|
|
480
941
|
|
|
481
942
|
Returns the registered locale names.
|
|
482
943
|
|
|
944
|
+
```js
|
|
945
|
+
msg.getDictionaryNames()
|
|
946
|
+
// => ['en', 'en-US']
|
|
947
|
+
```
|
|
948
|
+
|
|
483
949
|
### `addTermToDictionary(locale, key, value)`
|
|
484
950
|
|
|
485
951
|
Adds or replaces a translation term for a locale.
|
|
486
952
|
|
|
953
|
+
```js
|
|
954
|
+
msg.addTermToDictionary('en', 'BYE', 'Goodbye')
|
|
955
|
+
msg.message('BYE')
|
|
956
|
+
// => 'Goodbye'
|
|
957
|
+
```
|
|
958
|
+
|
|
487
959
|
### `getTermFromDictionary(locale, key)`
|
|
488
960
|
|
|
489
961
|
Returns a term value, or `undefined`.
|
|
490
962
|
|
|
963
|
+
```js
|
|
964
|
+
msg.getTermFromDictionary('en', 'HELLO')
|
|
965
|
+
// => 'Hello, {{name}}.'
|
|
966
|
+
```
|
|
967
|
+
|
|
491
968
|
### `getRawMessage(key, locales?)`
|
|
492
969
|
|
|
493
970
|
Returns the untranslated template string selected by locale lookup.
|
|
494
971
|
|
|
972
|
+
```js
|
|
973
|
+
msg.getRawMessage('HELLO')
|
|
974
|
+
// => 'Hello, {{name}}.'
|
|
975
|
+
```
|
|
976
|
+
|
|
495
977
|
### `message(key, values?)`
|
|
496
978
|
|
|
497
979
|
Formats and returns the final message string.
|
|
498
980
|
|
|
981
|
+
```js
|
|
982
|
+
msg.message('HELLO', { name: 'Taylor' })
|
|
983
|
+
// => 'Hello, Taylor.'
|
|
984
|
+
```
|
|
985
|
+
|
|
499
986
|
### `registerFormatter(name, fn)`
|
|
500
987
|
|
|
501
988
|
Registers a custom formatter callback.
|
|
502
989
|
|
|
990
|
+
```js
|
|
991
|
+
msg.registerFormatter('capitalize', ({ value }) => {
|
|
992
|
+
const text = value == null ? '' : String(value)
|
|
993
|
+
return text ? text[0].toUpperCase() + text.slice(1).toLowerCase() : text
|
|
994
|
+
})
|
|
995
|
+
```
|
|
996
|
+
|
|
503
997
|
Dictionary formatter configs may also set `postFormat` to the name of a registered formatter. Only two stages are supported: `format`, then `postFormat`.
|
|
504
998
|
|
|
505
999
|
## Development
|
|
@@ -512,6 +1006,37 @@ npm test
|
|
|
512
1006
|
|
|
513
1007
|
Tests currently build the package first, then run Mocha with `nyc` coverage.
|
|
514
1008
|
|
|
1009
|
+
## Publishing
|
|
1010
|
+
|
|
1011
|
+
Manual publish:
|
|
1012
|
+
|
|
1013
|
+
```sh
|
|
1014
|
+
npm publish --access public
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Trusted publishing via GitHub Actions is also configured in [publish.yml](/Users/seongnohyi/Workspace/work/intl-msg/.github/workflows/publish.yml).
|
|
1018
|
+
|
|
1019
|
+
Current workflow behavior:
|
|
1020
|
+
|
|
1021
|
+
- pushes of tags matching `v*` trigger the publish workflow
|
|
1022
|
+
- the workflow runs `npm ci`, `npm test`, and then `npm publish --access public`
|
|
1023
|
+
- npm trusted publishing must be configured on npmjs.com for this repository and workflow file
|
|
1024
|
+
|
|
1025
|
+
To enable trusted publishing on npm:
|
|
1026
|
+
|
|
1027
|
+
1. Open your package settings on npmjs.com
|
|
1028
|
+
2. Add a trusted publisher for GitHub Actions
|
|
1029
|
+
3. Use GitHub owner `eouia`
|
|
1030
|
+
4. Use repository `intl-msg`
|
|
1031
|
+
5. Use workflow file `publish.yml`
|
|
1032
|
+
|
|
1033
|
+
After that, publishing a new release is:
|
|
1034
|
+
|
|
1035
|
+
```sh
|
|
1036
|
+
git tag v0.1.0
|
|
1037
|
+
git push origin v0.1.0
|
|
1038
|
+
```
|
|
1039
|
+
|
|
515
1040
|
## Production use
|
|
516
1041
|
|
|
517
1042
|
`intl-msg` is usable in real applications today, especially when you want:
|
package/package.json
CHANGED