@ng-linguo/linguo 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/README.md +25 -0
- package/fesm2022/ng-linguo-linguo-http.mjs +32 -0
- package/fesm2022/ng-linguo-linguo-http.mjs.map +1 -0
- package/fesm2022/ng-linguo-linguo-icu.mjs +104 -0
- package/fesm2022/ng-linguo-linguo-icu.mjs.map +1 -0
- package/fesm2022/ng-linguo-linguo.mjs +775 -0
- package/fesm2022/ng-linguo-linguo.mjs.map +1 -0
- package/package.json +59 -0
- package/types/ng-linguo-linguo-http.d.ts +30 -0
- package/types/ng-linguo-linguo-icu.d.ts +86 -0
- package/types/ng-linguo-linguo.d.ts +454 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img
|
|
3
|
+
src="https://raw.githubusercontent.com/jmwierzbicki/linguo/main/apps/playground/public/linguo-logo.png"
|
|
4
|
+
alt="ng-linguo"
|
|
5
|
+
width="640"
|
|
6
|
+
/>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# @ng-linguo/linguo
|
|
10
|
+
|
|
11
|
+
A modern, complete i18n toolkit for Angular 21+, built on SignalStore.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { provideTranslate } from '@ng-linguo/linguo';
|
|
15
|
+
import { provideIcu } from '@ng-linguo/linguo/icu';
|
|
16
|
+
import { createHttpLoader } from '@ng-linguo/linguo/http';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **`@ng-linguo/linguo`** — the runtime: `TranslateStore`, the `t` pipe, the
|
|
20
|
+
`[t]` directive, `injectTranslate`, slot tags.
|
|
21
|
+
- **`@ng-linguo/linguo/icu`** — ICU MessageFormat (MF1 + MF2) via `provideIcu`.
|
|
22
|
+
- **`@ng-linguo/linguo/http`** — an HTTP `TranslationLoader` (`createHttpLoader`).
|
|
23
|
+
|
|
24
|
+
The build-time extraction CLI lives in the separate `@ng-linguo/extract`
|
|
25
|
+
package (pure Node, no Angular dependency).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HttpClient } from '@angular/common/http';
|
|
2
|
+
import { inject } from '@angular/core';
|
|
3
|
+
import { firstValueFrom } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a {@link TranslationLoader} backed by Angular's `HttpClient`.
|
|
7
|
+
*
|
|
8
|
+
* Must be called within an injection context (it injects `HttpClient`), so
|
|
9
|
+
* construct it inside a factory provider or an `inject`-aware bootstrap path
|
|
10
|
+
* where `provideHttpClient()` is also available. RxJS is used only here, at the
|
|
11
|
+
* I/O boundary, and is converted to the promise the loader interface expects.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const loader = createHttpLoader({ prefix: '/i18n/' });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function createHttpLoader(options = {}) {
|
|
19
|
+
const http = inject(HttpClient);
|
|
20
|
+
const prefix = options.prefix ?? '/assets/i18n/';
|
|
21
|
+
const suffix = options.suffix ?? '.json';
|
|
22
|
+
return {
|
|
23
|
+
load: (lang) => firstValueFrom(http.get(`${prefix}${lang}${suffix}`)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generated bundle index. Do not edit.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export { createHttpLoader };
|
|
32
|
+
//# sourceMappingURL=ng-linguo-linguo-http.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ng-linguo-linguo-http.mjs","sources":["../../../../packages/linguo/http/src/lib/http-loader.ts","../../../../packages/linguo/http/src/ng-linguo-linguo-http.ts"],"sourcesContent":["import { HttpClient } from '@angular/common/http';\nimport { inject } from '@angular/core';\nimport type { TranslationLoader, Translations } from '@ng-linguo/linguo';\nimport { firstValueFrom } from 'rxjs';\n\n/**\n * Options controlling where {@link createHttpLoader} fetches translation files\n * from. Files are expected to live at `${prefix}${lang}${suffix}` and contain a\n * flat JSON map of keys to strings.\n */\nexport interface HttpLoaderOptions {\n /** Path prefix for translation files. Defaults to `/assets/i18n/`. */\n readonly prefix?: string;\n /** File extension for translation files. Defaults to `.json`. */\n readonly suffix?: string;\n}\n\n/**\n * Create a {@link TranslationLoader} backed by Angular's `HttpClient`.\n *\n * Must be called within an injection context (it injects `HttpClient`), so\n * construct it inside a factory provider or an `inject`-aware bootstrap path\n * where `provideHttpClient()` is also available. RxJS is used only here, at the\n * I/O boundary, and is converted to the promise the loader interface expects.\n *\n * @example\n * ```ts\n * const loader = createHttpLoader({ prefix: '/i18n/' });\n * ```\n */\nexport function createHttpLoader(options: HttpLoaderOptions = {}): TranslationLoader {\n const http = inject(HttpClient);\n const prefix = options.prefix ?? '/assets/i18n/';\n const suffix = options.suffix ?? '.json';\n return {\n load: (lang: string): Promise<Translations> =>\n firstValueFrom(http.get<Translations>(`${prefix}${lang}${suffix}`)),\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAiBA;;;;;;;;;;;;AAYG;AACG,SAAU,gBAAgB,CAAC,OAAA,GAA6B,EAAE,EAAA;AAC9D,IAAA,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC;AAC/B,IAAA,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,eAAe;AAChD,IAAA,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO;IACxC,OAAO;QACL,IAAI,EAAE,CAAC,IAAY,KACjB,cAAc,CAAC,IAAI,CAAC,GAAG,CAAe,CAAA,EAAG,MAAM,CAAA,EAAG,IAAI,GAAG,MAAM,CAAA,CAAE,CAAC,CAAC;KACtE;AACH;;ACtCA;;AAEG;;;;"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { IntlMessageFormat } from 'intl-messageformat';
|
|
2
|
+
import { MessageFormat } from 'messageformat';
|
|
3
|
+
import { makeEnvironmentProviders } from '@angular/core';
|
|
4
|
+
import { MESSAGE_FORMATTER } from '@ng-linguo/linguo';
|
|
5
|
+
|
|
6
|
+
const SEP = String.fromCharCode(0);
|
|
7
|
+
const compilers = new Map();
|
|
8
|
+
function compile(message, locale, format) {
|
|
9
|
+
const cacheKey = `${format}${SEP}${locale}${SEP}${message}`;
|
|
10
|
+
const cached = compilers.get(cacheKey);
|
|
11
|
+
if (cached) {
|
|
12
|
+
return cached;
|
|
13
|
+
}
|
|
14
|
+
let render;
|
|
15
|
+
if (format === 'mf1') {
|
|
16
|
+
const mf = new IntlMessageFormat(message, locale);
|
|
17
|
+
render = (args) => {
|
|
18
|
+
const result = mf.format(args);
|
|
19
|
+
return Array.isArray(result) ? result.join('') : String(result);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// Disable bidi isolation so output is plain text (no invisible U+2068/U+2069
|
|
24
|
+
// marks around placeholders), matching MF1 and keeping output predictable.
|
|
25
|
+
const mf = new MessageFormat(locale, message, { bidiIsolation: 'none' });
|
|
26
|
+
render = (args) => mf.format(args);
|
|
27
|
+
}
|
|
28
|
+
compilers.set(cacheKey, render);
|
|
29
|
+
return render;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Format an ICU message against `args` for a locale.
|
|
33
|
+
*
|
|
34
|
+
* Defaults to MessageFormat 2.0; pass `format: 'mf1'` for classic ICU. Compiled
|
|
35
|
+
* messages are memoized per `(format, locale, message)`. On a malformed or
|
|
36
|
+
* unsupported pattern the raw message is returned rather than throwing, so a
|
|
37
|
+
* bad string never breaks the view.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* formatMessage('{n, plural, one {# file} other {# files}}', { n: 3 }, { locale: 'en', format: 'mf1' });
|
|
42
|
+
* // '3 files'
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function formatMessage(message, args, options) {
|
|
46
|
+
try {
|
|
47
|
+
return compile(message, options.locale, options.format ?? 'mf2')(args);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return message;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enable ICU message formatting for the `translate` pipe.
|
|
56
|
+
*
|
|
57
|
+
* Add it next to `provideTranslate`. Once provided, any `translate` call given
|
|
58
|
+
* an arguments object formats the message as ICU — consumers never touch a
|
|
59
|
+
* separate ICU pipe or the formatter directly. Defaults to MessageFormat 2.0;
|
|
60
|
+
* pass `{ defaultFormat: 'mf1' }` for classic ICU.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* bootstrapApplication(App, {
|
|
65
|
+
* providers: [
|
|
66
|
+
* provideTranslate({ defaultLang: 'en', loader }),
|
|
67
|
+
* provideIcu(),
|
|
68
|
+
* ],
|
|
69
|
+
* });
|
|
70
|
+
* // template: {{ '{count, plural, one {# file} other {# files}}' | translate: { count } }}
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
function provideIcu(config) {
|
|
74
|
+
const format = config?.defaultFormat ?? 'mf2';
|
|
75
|
+
const formatter = {
|
|
76
|
+
format: (message, args, locale) => formatMessage(message, args, { locale, format }),
|
|
77
|
+
};
|
|
78
|
+
return makeEnvironmentProviders([{ provide: MESSAGE_FORMATTER, useValue: formatter }]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Select the CLDR plural category for `count` in `locale`, used to pick the
|
|
83
|
+
* correct ICU plural branch.
|
|
84
|
+
*
|
|
85
|
+
* Backed by the platform `Intl.PluralRules`, so it has no Angular dependency
|
|
86
|
+
* and runs anywhere ICU formatting is needed. The locale is always explicit —
|
|
87
|
+
* the runtime never reads the ambient browser locale.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* selectPlural(1, 'en'); // 'one'
|
|
92
|
+
* selectPlural(5, 'en'); // 'other'
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function selectPlural(count, locale) {
|
|
96
|
+
return new Intl.PluralRules(locale).select(count);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generated bundle index. Do not edit.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
export { formatMessage, provideIcu, selectPlural };
|
|
104
|
+
//# sourceMappingURL=ng-linguo-linguo-icu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ng-linguo-linguo-icu.mjs","sources":["../../../../packages/linguo/icu/src/lib/format.ts","../../../../packages/linguo/icu/src/lib/provide-icu.ts","../../../../packages/linguo/icu/src/lib/plural.ts","../../../../packages/linguo/icu/src/ng-linguo-linguo-icu.ts"],"sourcesContent":["import { IntlMessageFormat } from 'intl-messageformat';\nimport { MessageFormat } from 'messageformat';\n\n/**\n * Which ICU message syntax a message is written in.\n *\n * - `'mf2'` — Unicode MessageFormat 2.0 (`.input`/`.match`/`{$var :number}`),\n * the default and future-facing format.\n * - `'mf1'` — classic ICU MessageFormat (`{count, plural, one {#} other {#}}`),\n * what most existing translation tooling emits today.\n */\nexport type MessageFormatVersion = 'mf2' | 'mf1';\n\n/**\n * Arguments substituted into a message. Values are the primitives the ICU\n * formatters accept (numbers for plurals/number formatting, strings for\n * `select`, `Date` for date/time).\n */\nexport type IcuArgs = Record<string, string | number | boolean | Date | null | undefined>;\n\n/** Options for {@link formatMessage}. */\nexport interface FormatMessageOptions {\n /** BCP-47 locale used for plural rules and number/date formatting. */\n readonly locale: string;\n /** Message syntax to parse. Defaults to `'mf2'`. */\n readonly format?: MessageFormatVersion;\n}\n\nconst SEP = String.fromCharCode(0);\nconst compilers = new Map<string, (args: IcuArgs) => string>();\n\nfunction compile(\n message: string,\n locale: string,\n format: MessageFormatVersion,\n): (args: IcuArgs) => string {\n const cacheKey = `${format}${SEP}${locale}${SEP}${message}`;\n const cached = compilers.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n let render: (args: IcuArgs) => string;\n if (format === 'mf1') {\n const mf = new IntlMessageFormat(message, locale);\n render = (args) => {\n const result = mf.format(args);\n return Array.isArray(result) ? result.join('') : String(result);\n };\n } else {\n // Disable bidi isolation so output is plain text (no invisible U+2068/U+2069\n // marks around placeholders), matching MF1 and keeping output predictable.\n const mf = new MessageFormat(locale, message, { bidiIsolation: 'none' });\n render = (args) => mf.format(args);\n }\n\n compilers.set(cacheKey, render);\n return render;\n}\n\n/**\n * Format an ICU message against `args` for a locale.\n *\n * Defaults to MessageFormat 2.0; pass `format: 'mf1'` for classic ICU. Compiled\n * messages are memoized per `(format, locale, message)`. On a malformed or\n * unsupported pattern the raw message is returned rather than throwing, so a\n * bad string never breaks the view.\n *\n * @example\n * ```ts\n * formatMessage('{n, plural, one {# file} other {# files}}', { n: 3 }, { locale: 'en', format: 'mf1' });\n * // '3 files'\n * ```\n */\nexport function formatMessage(\n message: string,\n args: IcuArgs,\n options: FormatMessageOptions,\n): string {\n try {\n return compile(message, options.locale, options.format ?? 'mf2')(args);\n } catch {\n return message;\n }\n}\n","import { type EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';\nimport { MESSAGE_FORMATTER, type MessageFormatter } from '@ng-linguo/linguo';\n\nimport { formatMessage, type IcuArgs, type MessageFormatVersion } from './format';\n\n/**\n * Enable ICU message formatting for the `translate` pipe.\n *\n * Add it next to `provideTranslate`. Once provided, any `translate` call given\n * an arguments object formats the message as ICU — consumers never touch a\n * separate ICU pipe or the formatter directly. Defaults to MessageFormat 2.0;\n * pass `{ defaultFormat: 'mf1' }` for classic ICU.\n *\n * @example\n * ```ts\n * bootstrapApplication(App, {\n * providers: [\n * provideTranslate({ defaultLang: 'en', loader }),\n * provideIcu(),\n * ],\n * });\n * // template: {{ '{count, plural, one {# file} other {# files}}' | translate: { count } }}\n * ```\n */\nexport function provideIcu(config?: {\n defaultFormat?: MessageFormatVersion;\n}): EnvironmentProviders {\n const format = config?.defaultFormat ?? 'mf2';\n const formatter: MessageFormatter = {\n format: (message, args, locale) => formatMessage(message, args as IcuArgs, { locale, format }),\n };\n return makeEnvironmentProviders([{ provide: MESSAGE_FORMATTER, useValue: formatter }]);\n}\n","/**\n * The CLDR plural categories. A given locale uses a subset of these; English,\n * for example, only distinguishes `'one'` and `'other'`.\n */\nexport type PluralCategory = Intl.LDMLPluralRule;\n\n/**\n * Select the CLDR plural category for `count` in `locale`, used to pick the\n * correct ICU plural branch.\n *\n * Backed by the platform `Intl.PluralRules`, so it has no Angular dependency\n * and runs anywhere ICU formatting is needed. The locale is always explicit —\n * the runtime never reads the ambient browser locale.\n *\n * @example\n * ```ts\n * selectPlural(1, 'en'); // 'one'\n * selectPlural(5, 'en'); // 'other'\n * ```\n */\nexport function selectPlural(count: number, locale: string): PluralCategory {\n return new Intl.PluralRules(locale).select(count);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AA4BA,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;AAClC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAqC;AAE9D,SAAS,OAAO,CACd,OAAe,EACf,MAAc,EACd,MAA4B,EAAA;AAE5B,IAAA,MAAM,QAAQ,GAAG,CAAA,EAAG,MAAM,CAAA,EAAG,GAAG,CAAA,EAAG,MAAM,CAAA,EAAG,GAAG,CAAA,EAAG,OAAO,EAAE;IAC3D,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC;IACtC,IAAI,MAAM,EAAE;AACV,QAAA,OAAO,MAAM;IACf;AAEA,IAAA,IAAI,MAAiC;AACrC,IAAA,IAAI,MAAM,KAAK,KAAK,EAAE;QACpB,MAAM,EAAE,GAAG,IAAI,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC;AACjD,QAAA,MAAM,GAAG,CAAC,IAAI,KAAI;YAChB,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;YAC9B,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;AACjE,QAAA,CAAC;IACH;SAAO;;;AAGL,QAAA,MAAM,EAAE,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;AACxE,QAAA,MAAM,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;IACpC;AAEA,IAAA,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC;AAC/B,IAAA,OAAO,MAAM;AACf;AAEA;;;;;;;;;;;;;AAaG;SACa,aAAa,CAC3B,OAAe,EACf,IAAa,EACb,OAA6B,EAAA;AAE7B,IAAA,IAAI;AACF,QAAA,OAAO,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC;IACxE;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,OAAO;IAChB;AACF;;AC/EA;;;;;;;;;;;;;;;;;;AAkBG;AACG,SAAU,UAAU,CAAC,MAE1B,EAAA;AACC,IAAA,MAAM,MAAM,GAAG,MAAM,EAAE,aAAa,IAAI,KAAK;AAC7C,IAAA,MAAM,SAAS,GAAqB;QAClC,MAAM,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,aAAa,CAAC,OAAO,EAAE,IAAe,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;KAC/F;AACD,IAAA,OAAO,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;AACxF;;AC1BA;;;;;;;;;;;;;AAaG;AACG,SAAU,YAAY,CAAC,KAAa,EAAE,MAAc,EAAA;AACxD,IAAA,OAAO,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;AACnD;;ACtBA;;AAEG;;;;"}
|