@frame-kit/ui-ng 0.2.9 → 0.2.10
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/COMPONENTS.md +8 -0
- package/fesm2022/frame-kit-ui-ng-services-i18n.mjs +97 -0
- package/fesm2022/frame-kit-ui-ng-services-i18n.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng.mjs +1 -0
- package/fesm2022/frame-kit-ui-ng.mjs.map +1 -1
- package/package.json +6 -1
- package/services/i18n/README.md +102 -0
- package/types/frame-kit-ui-ng-services-i18n.d.ts +81 -0
- package/types/frame-kit-ui-ng.d.ts +1 -0
package/COMPONENTS.md
CHANGED
|
@@ -83,6 +83,7 @@ Organized by **purpose** (not Atomic Design) into six layers with strict depende
|
|
|
83
83
|
### Services — runtime services with no UI
|
|
84
84
|
|
|
85
85
|
- [OverlayOrchestrator](#overlayorchestrator)
|
|
86
|
+
- [ReactiveI18nService](#reactivei18nservice)
|
|
86
87
|
- [SpotlightService](#spotlightservice)
|
|
87
88
|
- [ToastService](#toastservice)
|
|
88
89
|
|
|
@@ -631,6 +632,13 @@ Centralized priority stack for dialogs, drawers, popovers, and navigation panels
|
|
|
631
632
|
|
|
632
633
|
---
|
|
633
634
|
|
|
635
|
+
### ReactiveI18nService
|
|
636
|
+
|
|
637
|
+
Message lookup by dot-path key with `{{var}}` interpolation and a minimal ICU plural subset, backed by signals so a locale switch re-renders every template reading it. Provided with `provideI18n`, injected via `I18N_SERVICE`; the framework-free core lives in `@frame-kit/utils` so a server can render the same copy.
|
|
638
|
+
[README](https://github.com/frame-kit/packages/blob/main/packages/ui-ng/services/i18n/README.md)
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
634
642
|
### SpotlightService
|
|
635
643
|
|
|
636
644
|
Coordinates a single global "active key" with reference-counted holders, so unrelated parts of the DOM can pulse together when the same key is activated. The `fkSpotlightTrigger` / `fkSpotlightTarget` directives are the typical consumers.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { I18nService, detectLocale, saveLocale } from '@frame-kit/utils';
|
|
2
|
+
import { signal, InjectionToken } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Angular face of `I18nService` from `@frame-kit/utils`.
|
|
6
|
+
*
|
|
7
|
+
* Adds three things the framework-agnostic base deliberately leaves out:
|
|
8
|
+
* signal-backed messages, so a template reading `get()` re-renders when the
|
|
9
|
+
* locale changes; browser locale detection at construction; and persistence of
|
|
10
|
+
* the visitor's choice. Everything else — lookup, interpolation, plurals,
|
|
11
|
+
* merging — comes from the base, which the backend can use unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Provided through {@link provideI18n} and injected via {@link I18N_SERVICE},
|
|
14
|
+
* one instance per application injector.
|
|
15
|
+
*/
|
|
16
|
+
class ReactiveI18nService extends I18nService {
|
|
17
|
+
localeSignal;
|
|
18
|
+
messagesSignal;
|
|
19
|
+
storageKey;
|
|
20
|
+
/** The active locale, as a signal. Read it in templates to react to changes. */
|
|
21
|
+
locale;
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
const supportedLocales = config.supportedLocales ?? ['en'];
|
|
24
|
+
super({
|
|
25
|
+
...config,
|
|
26
|
+
supportedLocales,
|
|
27
|
+
locale: config.locale ??
|
|
28
|
+
detectLocale({ supportedLocales, storageKey: config.storageKey }),
|
|
29
|
+
});
|
|
30
|
+
this.storageKey = config.storageKey;
|
|
31
|
+
this.localeSignal = signal(this.getLocale(), ...(ngDevMode ? [{ debugName: "localeSignal" }] : /* istanbul ignore next */ []));
|
|
32
|
+
this.locale = this.localeSignal.asReadonly();
|
|
33
|
+
this.messagesSignal = signal(this.messages, ...(ngDevMode ? [{ debugName: "messagesSignal" }] : /* istanbul ignore next */ []));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Switch locale and remember the choice. Ignores an unsupported locale, so
|
|
37
|
+
* the current one survives.
|
|
38
|
+
*/
|
|
39
|
+
setLocale(locale) {
|
|
40
|
+
// The base rebuilds `messages` and calls onLocaleChanged(), which syncs
|
|
41
|
+
// the signals below.
|
|
42
|
+
super.setLocale(locale);
|
|
43
|
+
if (this.storageKey) {
|
|
44
|
+
saveLocale(this.storageKey, this.getLocale());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Reads from the signal, which is what makes `get()` reactive. */
|
|
48
|
+
currentMessages() {
|
|
49
|
+
return this.messagesSignal();
|
|
50
|
+
}
|
|
51
|
+
onLocaleChanged() {
|
|
52
|
+
// The base constructor builds messages before our fields are initialized,
|
|
53
|
+
// and it calls this hook on the way. Nothing to sync yet at that point.
|
|
54
|
+
if (!this.localeSignal) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.localeSignal.set(this.getLocale());
|
|
58
|
+
this.messagesSignal.set(this.messages);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Inject the app's i18n service.
|
|
63
|
+
*
|
|
64
|
+
* Typed as the reactive subclass: `get()` is signal-backed, and `locale` is
|
|
65
|
+
* readable in templates.
|
|
66
|
+
*/
|
|
67
|
+
const I18N_SERVICE = new InjectionToken('FK_I18N_SERVICE');
|
|
68
|
+
/**
|
|
69
|
+
* Provide i18n for the application.
|
|
70
|
+
*
|
|
71
|
+
* Single locale:
|
|
72
|
+
* ```ts
|
|
73
|
+
* provideI18n({ sources: [appMessages] })
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* Several, with the choice remembered:
|
|
77
|
+
* ```ts
|
|
78
|
+
* provideI18n({
|
|
79
|
+
* supportedLocales: ['en', 'es'],
|
|
80
|
+
* localeMaps: [{ en: enMessages, es: esMessages }],
|
|
81
|
+
* storageKey: 'acme-locale',
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function provideI18n(config = {}) {
|
|
86
|
+
return {
|
|
87
|
+
provide: I18N_SERVICE,
|
|
88
|
+
useFactory: () => new ReactiveI18nService(config),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generated bundle index. Do not edit.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
export { I18N_SERVICE, ReactiveI18nService, provideI18n };
|
|
97
|
+
//# sourceMappingURL=frame-kit-ui-ng-services-i18n.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-kit-ui-ng-services-i18n.mjs","sources":["../../../../packages/ui-ng/services/i18n/i18n.service.ts","../../../../packages/ui-ng/services/i18n/frame-kit-ui-ng-services-i18n.ts"],"sourcesContent":["import type { LocaleMap, Messages } from '@frame-kit/utils';\nimport { detectLocale, I18nService, saveLocale } from '@frame-kit/utils';\nimport type { Provider, Signal, WritableSignal } from '@angular/core';\nimport { InjectionToken, signal } from '@angular/core';\n\n/** Everything `provideI18n` needs to build the service. */\nexport interface I18nConfig {\n /**\n * Force a locale. Omit to detect one from storage, then the browser\n * language, then `supportedLocales[0]`.\n */\n locale?: string;\n\n /** Locale codes the app ships messages for. Defaults to `['en']`. */\n supportedLocales?: string[];\n\n /** Message bundles keyed by locale — merged in order, later wins. */\n localeMaps?: LocaleMap[];\n\n /** Locale-independent bundles, merged after `localeMaps`. */\n sources?: Messages[];\n\n /**\n * `localStorage` key for the visitor's chosen locale. Omit and the choice\n * lasts for the session only.\n *\n * Name it per product (`'acme-locale'`): two apps on one origin sharing a\n * key overwrite each other's preference.\n */\n storageKey?: string;\n}\n\n/**\n * The Angular face of `I18nService` from `@frame-kit/utils`.\n *\n * Adds three things the framework-agnostic base deliberately leaves out:\n * signal-backed messages, so a template reading `get()` re-renders when the\n * locale changes; browser locale detection at construction; and persistence of\n * the visitor's choice. Everything else — lookup, interpolation, plurals,\n * merging — comes from the base, which the backend can use unchanged.\n *\n * Provided through {@link provideI18n} and injected via {@link I18N_SERVICE},\n * one instance per application injector.\n */\nexport class ReactiveI18nService extends I18nService {\n private readonly localeSignal: WritableSignal<string>;\n private readonly messagesSignal: WritableSignal<Messages>;\n private readonly storageKey?: string;\n\n /** The active locale, as a signal. Read it in templates to react to changes. */\n readonly locale: Signal<string>;\n\n constructor(config: I18nConfig = {}) {\n const supportedLocales = config.supportedLocales ?? ['en'];\n\n super({\n ...config,\n supportedLocales,\n locale:\n config.locale ??\n detectLocale({ supportedLocales, storageKey: config.storageKey }),\n });\n\n this.storageKey = config.storageKey;\n this.localeSignal = signal(this.getLocale());\n this.locale = this.localeSignal.asReadonly();\n this.messagesSignal = signal(this.messages);\n }\n\n /**\n * Switch locale and remember the choice. Ignores an unsupported locale, so\n * the current one survives.\n */\n override setLocale(locale: string): void {\n // The base rebuilds `messages` and calls onLocaleChanged(), which syncs\n // the signals below.\n super.setLocale(locale);\n\n if (this.storageKey) {\n saveLocale(this.storageKey, this.getLocale());\n }\n }\n\n /** Reads from the signal, which is what makes `get()` reactive. */\n protected override currentMessages(): Messages {\n return this.messagesSignal();\n }\n\n protected override onLocaleChanged(): void {\n // The base constructor builds messages before our fields are initialized,\n // and it calls this hook on the way. Nothing to sync yet at that point.\n if (!this.localeSignal) {\n return;\n }\n\n this.localeSignal.set(this.getLocale());\n this.messagesSignal.set(this.messages);\n }\n}\n\n/**\n * Inject the app's i18n service.\n *\n * Typed as the reactive subclass: `get()` is signal-backed, and `locale` is\n * readable in templates.\n */\nexport const I18N_SERVICE = new InjectionToken<ReactiveI18nService>(\n 'FK_I18N_SERVICE',\n);\n\n/**\n * Provide i18n for the application.\n *\n * Single locale:\n * ```ts\n * provideI18n({ sources: [appMessages] })\n * ```\n *\n * Several, with the choice remembered:\n * ```ts\n * provideI18n({\n * supportedLocales: ['en', 'es'],\n * localeMaps: [{ en: enMessages, es: esMessages }],\n * storageKey: 'acme-locale',\n * })\n * ```\n */\nexport function provideI18n(config: I18nConfig = {}): Provider {\n return {\n provide: I18N_SERVICE,\n useFactory: () => new ReactiveI18nService(config),\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAgCA;;;;;;;;;;;AAWG;AACG,MAAO,mBAAoB,SAAQ,WAAW,CAAA;AACjC,IAAA,YAAY;AACZ,IAAA,cAAc;AACd,IAAA,UAAU;;AAGlB,IAAA,MAAM;AAEf,IAAA,WAAA,CAAY,SAAqB,EAAE,EAAA;QACjC,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,CAAC,IAAI,CAAC;AAE1D,QAAA,KAAK,CAAC;AACJ,YAAA,GAAG,MAAM;YACT,gBAAgB;YAChB,MAAM,EACJ,MAAM,CAAC,MAAM;gBACb,YAAY,CAAC,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC;AACpE,SAAA,CAAC;AAEF,QAAA,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU;QACnC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,cAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAC;QAC5C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE;QAC5C,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,gBAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAC;IAC7C;AAEA;;;AAGG;AACM,IAAA,SAAS,CAAC,MAAc,EAAA;;;AAG/B,QAAA,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC;AAEvB,QAAA,IAAI,IAAI,CAAC,UAAU,EAAE;YACnB,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/C;IACF;;IAGmB,eAAe,GAAA;AAChC,QAAA,OAAO,IAAI,CAAC,cAAc,EAAE;IAC9B;IAEmB,eAAe,GAAA;;;AAGhC,QAAA,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE;YACtB;QACF;QAEA,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACvC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;IACxC;AACD;AAED;;;;;AAKG;MACU,YAAY,GAAG,IAAI,cAAc,CAC5C,iBAAiB;AAGnB;;;;;;;;;;;;;;;;AAgBG;AACG,SAAU,WAAW,CAAC,MAAA,GAAqB,EAAE,EAAA;IACjD,OAAO;AACL,QAAA,OAAO,EAAE,YAAY;QACrB,UAAU,EAAE,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC;KAClD;AACH;;ACpIA;;AAEG;;;;"}
|
|
@@ -10,6 +10,7 @@ export * from '@frame-kit/ui-ng/directives/tooltip';
|
|
|
10
10
|
export * from '@frame-kit/ui-ng/forms';
|
|
11
11
|
export * from '@frame-kit/ui-ng/layouts/app-shell';
|
|
12
12
|
export * from '@frame-kit/ui-ng/layouts/content-split';
|
|
13
|
+
export * from '@frame-kit/ui-ng/services/i18n';
|
|
13
14
|
export * from '@frame-kit/ui-ng/services/spotlight';
|
|
14
15
|
export * from '@frame-kit/ui-ng/services/toast';
|
|
15
16
|
export * from '@frame-kit/ui-ng/ui/accordion';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame-kit-ui-ng.mjs","sources":["../../../../packages/ui-ng/frame-kit-ui-ng.ts"],"sourcesContent":["/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"frame-kit-ui-ng.mjs","sources":["../../../../packages/ui-ng/frame-kit-ui-ng.ts"],"sourcesContent":["/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;AAEG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frame-kit/ui-ng",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Style-agnostic, token-driven Angular UI component library for FrameKit.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"@angular/forms": "^21.2.0",
|
|
18
18
|
"@angular/platform-browser": "^21.2.0",
|
|
19
19
|
"@angular/router": "^21.2.0",
|
|
20
|
+
"@frame-kit/utils": "0.2.10",
|
|
20
21
|
"rxjs": "^7.0.0"
|
|
21
22
|
},
|
|
22
23
|
"sideEffects": false,
|
|
@@ -81,6 +82,10 @@
|
|
|
81
82
|
"types": "./types/frame-kit-ui-ng-layouts-content-split.d.ts",
|
|
82
83
|
"default": "./fesm2022/frame-kit-ui-ng-layouts-content-split.mjs"
|
|
83
84
|
},
|
|
85
|
+
"./services/i18n": {
|
|
86
|
+
"types": "./types/frame-kit-ui-ng-services-i18n.d.ts",
|
|
87
|
+
"default": "./fesm2022/frame-kit-ui-ng-services-i18n.mjs"
|
|
88
|
+
},
|
|
84
89
|
"./services/spotlight": {
|
|
85
90
|
"types": "./types/frame-kit-ui-ng-services-spotlight.d.ts",
|
|
86
91
|
"default": "./fesm2022/frame-kit-ui-ng-services-spotlight.mjs"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# I18n
|
|
2
|
+
|
|
3
|
+
Message lookup for Angular applications: dot-path keys, `{{var}}` interpolation, plurals, and locale switching that re-renders templates.
|
|
4
|
+
|
|
5
|
+
`ReactiveI18nService` is the Angular face of `I18nService` from `@frame-kit/utils`. The base does the work — resolution, interpolation, merging bundles — and stays framework-free so a server can render the same copy. This subclass adds the three things only a browser app needs: signal-backed messages, locale detection at construction, and persistence of the visitor's choice.
|
|
6
|
+
|
|
7
|
+
## Initialization
|
|
8
|
+
|
|
9
|
+
Provided **per application injector** through `provideI18n`, and injected via the `I18N_SERVICE` token. Construction reads `localStorage` and `navigator.language` once; there are no subscriptions or listeners to clean up.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// app.config.ts
|
|
13
|
+
import { provideI18n } from '@frame-kit/ui-ng/services/i18n';
|
|
14
|
+
|
|
15
|
+
import enMessages from './locales/en.json';
|
|
16
|
+
import esMessages from './locales/es.json';
|
|
17
|
+
|
|
18
|
+
export const appConfig: ApplicationConfig = {
|
|
19
|
+
providers: [
|
|
20
|
+
provideI18n({
|
|
21
|
+
supportedLocales: ['en', 'es'],
|
|
22
|
+
localeMaps: [{ en: enMessages, es: esMessages }],
|
|
23
|
+
storageKey: 'acme-locale',
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A single-locale app needs only its bundles:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
provideI18n({ sources: [appMessages] });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Config
|
|
36
|
+
|
|
37
|
+
| Option | Type | Purpose |
|
|
38
|
+
| ------------------ | ------------- | ----------------------------------------------------------------------------------- |
|
|
39
|
+
| `locale` | `string` | Force a locale and skip detection. |
|
|
40
|
+
| `supportedLocales` | `string[]` | Locale codes the app ships messages for. Defaults to `['en']`. |
|
|
41
|
+
| `localeMaps` | `LocaleMap[]` | Bundles keyed by locale, merged in order — later sources win. |
|
|
42
|
+
| `sources` | `Messages[]` | Locale-independent bundles, merged after `localeMaps`. |
|
|
43
|
+
| `storageKey` | `string` | `localStorage` key for the visitor's choice. Omit and the choice lasts the session. |
|
|
44
|
+
|
|
45
|
+
Name `storageKey` per product (`'acme-locale'`). Two apps on one origin sharing a key would overwrite each other's preference.
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
| Member | Signature | Behavior |
|
|
50
|
+
| --------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `locale` | `Signal<string>` | The active locale code. Read it in a template to react to changes. |
|
|
52
|
+
| `get` | `(key: string, vars?: Record<string, string \| number>) => string` | Resolves a dot-path key and interpolates. Returns the **key itself** when the message is missing, so the gap is visible in review rather than blank on the page. |
|
|
53
|
+
| `getRaw` | `<T>(path: string, vars?) => T \| undefined` | Returns a non-string value — an array of steps, a table of rows — interpolating every string inside it while preserving shape. |
|
|
54
|
+
| `setLocale` | `(locale: string) => void` | Switches locale and persists it when `storageKey` is set. Ignores an unsupported locale, leaving the current one active. |
|
|
55
|
+
| `getLocale` | `() => string` | The active locale as a plain string, for non-reactive callers. |
|
|
56
|
+
| `getSupportedLocales` | `() => string[]` | The configured locale codes. |
|
|
57
|
+
| `registerLocaleMap` | `(map: LocaleMap) => void` | Merges a bundle in at runtime and re-renders. For lazy routes that load their own copy on activation rather than at boot. |
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
@Component({
|
|
63
|
+
selector: 'app-sign-in',
|
|
64
|
+
template: `
|
|
65
|
+
<h1>{{ i18n.get('auth.signIn.title') }}</h1>
|
|
66
|
+
<p>{{ i18n.get('auth.signIn.greeting', { name: user().firstName }) }}</p>
|
|
67
|
+
<p>{{ i18n.get('auth.mfa.codesLeft', { n: remaining() }) }}</p>
|
|
68
|
+
`,
|
|
69
|
+
})
|
|
70
|
+
export class SignInComponent {
|
|
71
|
+
protected readonly i18n = inject(I18N_SERVICE);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
with a bundle shaped to match:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"auth": {
|
|
80
|
+
"signIn": { "title": "Sign in", "greeting": "Welcome back, {{name}}" },
|
|
81
|
+
"mfa": {
|
|
82
|
+
"codesLeft": "{n, plural, =0 {No codes left} =1 {One code left} other {# codes left}}"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Behavior Notes
|
|
89
|
+
|
|
90
|
+
**Reactivity.** `get()` reads messages from a signal, so any template calling it re-renders on `setLocale` or `registerLocaleMap` without the component subscribing to anything. Values captured into a plain field at construction will **not** update — call `get()` in the template, or wrap it in a `computed`.
|
|
91
|
+
|
|
92
|
+
**Missing keys render as the key.** There is no fallback locale chain: a key absent from the active locale's bundle renders as `auth.signIn.title`. Loud on purpose.
|
|
93
|
+
|
|
94
|
+
**Unpassed variables render as written.** `Hello {{name}}` with no `name` keeps the placeholder, naming the variable that was forgotten.
|
|
95
|
+
|
|
96
|
+
**Plurals are a minimal ICU subset.** `{count, plural, =0 {…} =1 {…} other {…}}` — exact selectors plus `other`, with `#` substituted for the value. No `zero`/`few`/`many` CLDR categories and no nested braces in branch bodies. Enough for counts in UI copy; reach for a full ICU formatter if a locale needs the rest.
|
|
97
|
+
|
|
98
|
+
**Merge order.** `localeMaps` merge in array order, then `sources`, each deep-merged over the last. A partial override replaces only the keys it names — siblings survive — which is how an app tailors a shared bundle without restating it.
|
|
99
|
+
|
|
100
|
+
**Detection order.** Stored locale, then `navigator.language`, then `defaultLocale`, then `supportedLocales[0]`. Each candidate is normalized (`en-US` → `en`) and must be supported to win. Under SSR both browser sources are absent and detection falls through to the default, so the server renders the default locale and the client may switch on hydration.
|
|
101
|
+
|
|
102
|
+
**Storage is best-effort.** Private-mode and blocked-cookie browsers throw on `localStorage` access; reads and writes swallow it. A remembered locale is a convenience, never a requirement.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { I18nService, LocaleMap, Messages } from '@frame-kit/utils';
|
|
2
|
+
import { InjectionToken, Signal, Provider } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/** Everything `provideI18n` needs to build the service. */
|
|
5
|
+
interface I18nConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Force a locale. Omit to detect one from storage, then the browser
|
|
8
|
+
* language, then `supportedLocales[0]`.
|
|
9
|
+
*/
|
|
10
|
+
locale?: string;
|
|
11
|
+
/** Locale codes the app ships messages for. Defaults to `['en']`. */
|
|
12
|
+
supportedLocales?: string[];
|
|
13
|
+
/** Message bundles keyed by locale — merged in order, later wins. */
|
|
14
|
+
localeMaps?: LocaleMap[];
|
|
15
|
+
/** Locale-independent bundles, merged after `localeMaps`. */
|
|
16
|
+
sources?: Messages[];
|
|
17
|
+
/**
|
|
18
|
+
* `localStorage` key for the visitor's chosen locale. Omit and the choice
|
|
19
|
+
* lasts for the session only.
|
|
20
|
+
*
|
|
21
|
+
* Name it per product (`'acme-locale'`): two apps on one origin sharing a
|
|
22
|
+
* key overwrite each other's preference.
|
|
23
|
+
*/
|
|
24
|
+
storageKey?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The Angular face of `I18nService` from `@frame-kit/utils`.
|
|
28
|
+
*
|
|
29
|
+
* Adds three things the framework-agnostic base deliberately leaves out:
|
|
30
|
+
* signal-backed messages, so a template reading `get()` re-renders when the
|
|
31
|
+
* locale changes; browser locale detection at construction; and persistence of
|
|
32
|
+
* the visitor's choice. Everything else — lookup, interpolation, plurals,
|
|
33
|
+
* merging — comes from the base, which the backend can use unchanged.
|
|
34
|
+
*
|
|
35
|
+
* Provided through {@link provideI18n} and injected via {@link I18N_SERVICE},
|
|
36
|
+
* one instance per application injector.
|
|
37
|
+
*/
|
|
38
|
+
declare class ReactiveI18nService extends I18nService {
|
|
39
|
+
private readonly localeSignal;
|
|
40
|
+
private readonly messagesSignal;
|
|
41
|
+
private readonly storageKey?;
|
|
42
|
+
/** The active locale, as a signal. Read it in templates to react to changes. */
|
|
43
|
+
readonly locale: Signal<string>;
|
|
44
|
+
constructor(config?: I18nConfig);
|
|
45
|
+
/**
|
|
46
|
+
* Switch locale and remember the choice. Ignores an unsupported locale, so
|
|
47
|
+
* the current one survives.
|
|
48
|
+
*/
|
|
49
|
+
setLocale(locale: string): void;
|
|
50
|
+
/** Reads from the signal, which is what makes `get()` reactive. */
|
|
51
|
+
protected currentMessages(): Messages;
|
|
52
|
+
protected onLocaleChanged(): void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Inject the app's i18n service.
|
|
56
|
+
*
|
|
57
|
+
* Typed as the reactive subclass: `get()` is signal-backed, and `locale` is
|
|
58
|
+
* readable in templates.
|
|
59
|
+
*/
|
|
60
|
+
declare const I18N_SERVICE: InjectionToken<ReactiveI18nService>;
|
|
61
|
+
/**
|
|
62
|
+
* Provide i18n for the application.
|
|
63
|
+
*
|
|
64
|
+
* Single locale:
|
|
65
|
+
* ```ts
|
|
66
|
+
* provideI18n({ sources: [appMessages] })
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* Several, with the choice remembered:
|
|
70
|
+
* ```ts
|
|
71
|
+
* provideI18n({
|
|
72
|
+
* supportedLocales: ['en', 'es'],
|
|
73
|
+
* localeMaps: [{ en: enMessages, es: esMessages }],
|
|
74
|
+
* storageKey: 'acme-locale',
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
declare function provideI18n(config?: I18nConfig): Provider;
|
|
79
|
+
|
|
80
|
+
export { I18N_SERVICE, ReactiveI18nService, provideI18n };
|
|
81
|
+
export type { I18nConfig };
|
|
@@ -10,6 +10,7 @@ export * from '@frame-kit/ui-ng/directives/tooltip';
|
|
|
10
10
|
export * from '@frame-kit/ui-ng/forms';
|
|
11
11
|
export * from '@frame-kit/ui-ng/layouts/app-shell';
|
|
12
12
|
export * from '@frame-kit/ui-ng/layouts/content-split';
|
|
13
|
+
export * from '@frame-kit/ui-ng/services/i18n';
|
|
13
14
|
export * from '@frame-kit/ui-ng/services/spotlight';
|
|
14
15
|
export * from '@frame-kit/ui-ng/services/toast';
|
|
15
16
|
export * from '@frame-kit/ui-ng/ui/accordion';
|