@mmstack/translate 21.1.11 → 21.1.13
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 +148 -26
- package/fesm2022/mmstack-translate.mjs +591 -143
- package/fesm2022/mmstack-translate.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-translate.d.ts +396 -83
package/README.md
CHANGED
|
@@ -164,7 +164,8 @@ export default createQuoteTranslation('sl-SI', {
|
|
|
164
164
|
errors: {
|
|
165
165
|
minLength: 'Citat mora imeti vsaj {min} znakov.', // Variables must match original
|
|
166
166
|
},
|
|
167
|
-
stats:
|
|
167
|
+
stats:
|
|
168
|
+
'{count, plural, =1 {# citat} =2 {# citata} few {# citati} other {# citatov}} na voljo',
|
|
168
169
|
});
|
|
169
170
|
```
|
|
170
171
|
|
|
@@ -223,7 +224,10 @@ export class QuoteTranslator extends Translator<QuoteLocale> {}
|
|
|
223
224
|
@Directive({
|
|
224
225
|
selector: '[translate]', // Input in Translate is named 'translate'
|
|
225
226
|
})
|
|
226
|
-
export class QuoteTranslate<TInput extends string> extends Translate<
|
|
227
|
+
export class QuoteTranslate<TInput extends string> extends Translate<
|
|
228
|
+
TInput,
|
|
229
|
+
QuoteLocale
|
|
230
|
+
> {}
|
|
227
231
|
```
|
|
228
232
|
|
|
229
233
|
### 3. Use Translations in Components
|
|
@@ -320,7 +324,8 @@ export const routes: Routes = [
|
|
|
320
324
|
children: [
|
|
321
325
|
{
|
|
322
326
|
path: 'quotes',
|
|
323
|
-
loadChildren: () =>
|
|
327
|
+
loadChildren: () =>
|
|
328
|
+
import('./quote/quote.routes').then((m) => m.QUOTE_ROUTES),
|
|
324
329
|
},
|
|
325
330
|
// ... other routes
|
|
326
331
|
],
|
|
@@ -451,9 +456,12 @@ export const createAppNamespace = ns.createMergedNamespace;
|
|
|
451
456
|
// common.t.ts
|
|
452
457
|
import { registerNamespace } from '@mmstack/translate';
|
|
453
458
|
|
|
454
|
-
const r = registerNamespace(
|
|
455
|
-
|
|
456
|
-
|
|
459
|
+
const r = registerNamespace(
|
|
460
|
+
() => import('./common.namespace').then((m) => m.default),
|
|
461
|
+
{
|
|
462
|
+
'sl-SI': () => import('./common-sl.translation').then((m) => m.default),
|
|
463
|
+
},
|
|
464
|
+
);
|
|
457
465
|
|
|
458
466
|
export const injectCommonT = r.injectNamespaceT;
|
|
459
467
|
export const resolveCommonTranslations = r.resolveNamespaceTranslation;
|
|
@@ -564,9 +572,13 @@ For cases where you need to load translations from a remote API (where keys aren
|
|
|
564
572
|
import { registerRemoteNamespace } from '@mmstack/translate';
|
|
565
573
|
|
|
566
574
|
// Returns an untyped t function: t('any.key')
|
|
567
|
-
const { injectNamespaceT: injectRemoteT } = registerRemoteNamespace(
|
|
568
|
-
'
|
|
569
|
-
|
|
575
|
+
const { injectNamespaceT: injectRemoteT } = registerRemoteNamespace(
|
|
576
|
+
'remote',
|
|
577
|
+
() => fetch('/api/en').then((r) => r.json()),
|
|
578
|
+
{
|
|
579
|
+
'sl-SI': () => fetch('/api/sl').then((r) => r.json()),
|
|
580
|
+
},
|
|
581
|
+
);
|
|
570
582
|
|
|
571
583
|
// usage
|
|
572
584
|
const t = injectRemoteT();
|
|
@@ -580,15 +592,7 @@ const valueThatNeedsProps = t('remote.myOtherKey', {
|
|
|
580
592
|
|
|
581
593
|
## Formatters
|
|
582
594
|
|
|
583
|
-
The library includes a set of reactive formatters that
|
|
584
|
-
|
|
585
|
-
**Note:** For reactivity, wrap them in a `computed()` if the input signals change or if you want them to react to dynamic locale changes.
|
|
586
|
-
|
|
587
|
-
**SSR note:** Formatters read the active locale from a process-level signal. This is safe for all client-side usage and for SSR on serverless platforms (Lambda, Vercel, Netlify Edge) or worker-thread pools, where each request runs in an isolated V8 context. If you run a traditional single-process Node.js SSR server and concurrently render pages for **different** locales, pass the locale explicitly via the `locale` option on each formatter to avoid a potential cross-request read:
|
|
588
|
-
|
|
589
|
-
```typescript
|
|
590
|
-
readonly displayDate = computed(() => formatDate(this.date, { locale: this.currentLocale() }));
|
|
591
|
-
```
|
|
595
|
+
The library includes a set of reactive formatters that wrap the standard `Intl.*` APIs and integrate with the dynamic locale signal.
|
|
592
596
|
|
|
593
597
|
Available formatters:
|
|
594
598
|
|
|
@@ -600,24 +604,82 @@ Available formatters:
|
|
|
600
604
|
- **`formatRelativeTime`**: Wraps `Intl.RelativeTimeFormat`
|
|
601
605
|
- **`formatDisplayName`**: Wraps `Intl.DisplayNames`
|
|
602
606
|
|
|
603
|
-
|
|
607
|
+
Each formatter ships in two flavors:
|
|
608
|
+
|
|
609
|
+
1. **Standalone function** (`formatDate`, `formatList`, …) — pass `locale` explicitly, either as a string or via the options object. Three overloads per formatter: `(value, locale)`, `(value, opt)` with a required `locale` field, and a deprecated unsafe form that omits the locale (kept for backwards compatibility, see SSR note below).
|
|
610
|
+
2. **`injectFormat*()` companion** (`injectFormatDate`, `injectFormatList`, …) — call inside an injection context to get a function that auto-resolves locale from `injectDynamicLocale()` and respects any defaults registered via `provideFormat*Defaults`. **This is the recommended path for components and services.**
|
|
611
|
+
|
|
612
|
+
You can grab them all at once via the `injectFormatters()` facade:
|
|
604
613
|
|
|
605
614
|
```typescript
|
|
606
615
|
import { computed, signal } from '@angular/core';
|
|
607
|
-
import {
|
|
616
|
+
import { injectFormatters } from '@mmstack/translate';
|
|
608
617
|
|
|
609
618
|
export class MyComponent {
|
|
619
|
+
private readonly fmt = injectFormatters();
|
|
620
|
+
|
|
610
621
|
readonly price = signal(1234.56);
|
|
611
622
|
readonly date = new Date();
|
|
612
623
|
|
|
613
|
-
// Reacts to price changes
|
|
614
|
-
readonly displayPrice = computed(() =>
|
|
615
|
-
|
|
616
|
-
// Reacts to locale changes
|
|
617
|
-
readonly displayDate = computed(() => formatDate(this.date));
|
|
624
|
+
// Reacts to price changes AND locale changes — no explicit locale needed
|
|
625
|
+
readonly displayPrice = computed(() => this.fmt.currency(this.price(), 'EUR'));
|
|
626
|
+
readonly displayDate = computed(() => this.fmt.date(this.date));
|
|
618
627
|
}
|
|
619
628
|
```
|
|
620
629
|
|
|
630
|
+
If you'd rather call the standalone forms (e.g. outside an injection context), pass the locale explicitly:
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
import { computed } from '@angular/core';
|
|
634
|
+
import { formatCurrency, formatDate, injectDynamicLocale } from '@mmstack/translate';
|
|
635
|
+
|
|
636
|
+
const locale = injectDynamicLocale();
|
|
637
|
+
|
|
638
|
+
readonly displayPrice = computed(() => formatCurrency(this.price(), 'EUR', { locale: locale() }));
|
|
639
|
+
readonly displayDate = computed(() => formatDate(this.date, locale()));
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Provider defaults
|
|
643
|
+
|
|
644
|
+
Each formatter has a paired `provideFormat*Defaults` provider, and they can be registered together via `provideFormatDefaults`:
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
import { provideFormatDefaults } from '@mmstack/translate';
|
|
648
|
+
|
|
649
|
+
bootstrapApplication(AppComponent, {
|
|
650
|
+
providers: [
|
|
651
|
+
provideFormatDefaults({
|
|
652
|
+
date: { format: 'mediumDate' },
|
|
653
|
+
number: { useGrouping: true, maxFractionDigits: 2 },
|
|
654
|
+
currency: { display: 'code' },
|
|
655
|
+
relativeTime: { numeric: 'auto' },
|
|
656
|
+
list: { type: 'disjunction' },
|
|
657
|
+
percent: { maxFractionDigits: 1 },
|
|
658
|
+
displayName: { style: 'short' },
|
|
659
|
+
}),
|
|
660
|
+
],
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
The injected formatter functions (`injectFormat*()`) automatically merge these defaults with the dynamic locale.
|
|
665
|
+
|
|
666
|
+
### SSR note
|
|
667
|
+
|
|
668
|
+
The deprecated unsafe overload (omitting `locale`) reads from a **process-level global signal**, which can cross-contaminate concurrent requests on a single-process Node.js SSR server rendering pages for different locales. The new overloads with required `locale` and the `injectFormat*()` companions are SSR-safe.
|
|
669
|
+
|
|
670
|
+
For SSR-bound code, prefer either:
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
// 1. Recommended — let the injected formatter handle locale
|
|
674
|
+
private readonly formatDate = injectFormatDate();
|
|
675
|
+
readonly displayDate = computed(() => this.formatDate(this.date));
|
|
676
|
+
|
|
677
|
+
// 2. Or pass locale explicitly to the standalone function
|
|
678
|
+
readonly displayDate = computed(() => formatDate(this.date, this.currentLocale()));
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
The unsafe overload is kept for backwards compatibility and will be removed in a future release.
|
|
682
|
+
|
|
621
683
|
## Testing
|
|
622
684
|
|
|
623
685
|
When testing components that use `@mmstack/translate` (via `injectNamespaceT`, `Translate` directive, or `Translator` pipe), you don't need to configure actual translated namespaces or deal with Intl loading. Instead, use the provided `provideMockTranslations()` utility.
|
|
@@ -643,7 +705,9 @@ describe('MyComponent', () => {
|
|
|
643
705
|
fixture.detectChanges();
|
|
644
706
|
|
|
645
707
|
// By default, it echoes back the flattened object key using dot notation
|
|
646
|
-
expect(fixture.nativeElement.textContent).toContain(
|
|
708
|
+
expect(fixture.nativeElement.textContent).toContain(
|
|
709
|
+
'myNamespace.greeting.title',
|
|
710
|
+
);
|
|
647
711
|
});
|
|
648
712
|
|
|
649
713
|
it('allows providing explicit mock overrides', () => {
|
|
@@ -704,6 +768,64 @@ If you're migrating from a runtime-only solution:
|
|
|
704
768
|
4. Convert your translation JSON files to TypeScript using `createNamespace`
|
|
705
769
|
5. Update component/template usage to use the type-safe APIs
|
|
706
770
|
|
|
771
|
+
## Escape Hatches
|
|
772
|
+
|
|
773
|
+
Sometimes we all hit the limit of an api & need imperitive escape hatches for those edge cases. These are the ones mmstack/translate currently provides:
|
|
774
|
+
|
|
775
|
+
**`withParams()`**
|
|
776
|
+
|
|
777
|
+
Type-level parameter inference is one level deep — variables inside `plural` / `select` / `selectordinal` arms aren't picked up (e.g. the `{name}` inside `{count, plural, one {Hi {name}} ...}`). For those cases, wrap the message with `withParams<P>(...)` to declare the missing params explicitly:
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
import { createNamespace, withParams } from '@mmstack/translate';
|
|
781
|
+
|
|
782
|
+
const ns = createNamespace('quote', {
|
|
783
|
+
// auto-extracts `count`; `name` is declared because it lives inside the arms
|
|
784
|
+
stats: withParams<{ name: string }>(
|
|
785
|
+
'{count, plural, one {1 quote from {name}} other {# quotes from {name}}}',
|
|
786
|
+
),
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// t inferred as: ('quote.stats', { count: number; name: string }) => string
|
|
790
|
+
t('quote.stats', { count: 3, name: 'Alice' });
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Declared params are merged with auto-extracted ones; on key conflict, declared wins. Non-default locales for a wrapped key don't need to repeat the helper — they accept any string:
|
|
794
|
+
|
|
795
|
+
```typescript
|
|
796
|
+
createQuoteTranslation('sl-SI', {
|
|
797
|
+
stats: '{count, plural, =1 {1 citat od {name}} other {# citatov od {name}}}',
|
|
798
|
+
});
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Trade-off: wrapping a key opts out of template-literal shape strictness for that key in non-default locales (the auto-validation that requires placeholders to appear in the right positions). The library still enforces top-level placeholders for non-wrapped keys.
|
|
802
|
+
|
|
803
|
+
**`injectAddTranslations()`**
|
|
804
|
+
Allows adding flat, per-locale translations to any namespace at runtime.
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
import { injectAddTranslations } from '@mmstack/translate';
|
|
808
|
+
|
|
809
|
+
const addTranslations = injectAddTranslations();
|
|
810
|
+
addTranslations('dynamicNs', {
|
|
811
|
+
'en-US': { greeting: 'Hello {name}!' },
|
|
812
|
+
'sl-SI': { greeting: 'Zdravo {name}!' },
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**`injectUnsafeT()`**
|
|
817
|
+
Returns a fully untyped translation function `t('anyNamespace.key')`. Ideal for reading dynamically added keys or cross-namespace lookups where the typed API would be impractical.
|
|
818
|
+
|
|
819
|
+
```typescript
|
|
820
|
+
import { injectUnsafeT } from '@mmstack/translate';
|
|
821
|
+
|
|
822
|
+
const t = injectUnsafeT();
|
|
823
|
+
const greeting = t('dynamicNs.greeting', { name: 'Alice' });
|
|
824
|
+
const signalGreeting = t.asSignal('dynamicNs.greeting', () => ({
|
|
825
|
+
name: 'Alice',
|
|
826
|
+
}));
|
|
827
|
+
```
|
|
828
|
+
|
|
707
829
|
## Contributing
|
|
708
830
|
|
|
709
831
|
Contributions, issues, and feature requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|