@magic-translate/angular 1.0.2 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0](https://github.com/magic-translate/magic-translate/compare/angular-v1.1.0...angular-v1.2.0) (2026-11-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * **angular:** translate with the secret key on the server ([5f6fae1](https://github.com/magic-translate/magic-translate/commit/5f6fae1fdbaebefe0cb7c62939eb3a03a8419950))
9
+ * id option to edit one use of a text on its own ([9127af3](https://github.com/magic-translate/magic-translate/commit/9127af36de4d63be076e8e192a16584968b4655a))
10
+
11
+ ## [1.1.0](https://github.com/magic-translate/magic-translate/compare/angular-v1.0.2...angular-v1.1.0) (2026-11-02)
12
+
13
+
14
+ ### Features
15
+
16
+ * **angular:** translate text with variables as one cached template ([2f60135](https://github.com/magic-translate/magic-translate/commit/2f60135f4b7b09719a575bee4176367cea0fc6e7))
17
+
3
18
  ## [1.0.2](https://github.com/magic-translate/magic-translate/compare/angular-v1.0.1...angular-v1.0.2) (2026-10-29)
4
19
 
5
20
 
package/README.md CHANGED
@@ -105,6 +105,50 @@ export class Search {}
105
105
 
106
106
  The pipe returns the original text until the translation has loaded.
107
107
 
108
+ ### Variables
109
+
110
+ Pass values that change, such as names or counts, in `[v]` and reference them
111
+ as `$name`. The text is translated once as a template, `Hi {0}, you have {1}
112
+ new messages`, and cached, however many different values it is shown with. The
113
+ values themselves are never sent to the API.
114
+
115
+ ```html
116
+ <t [v]="{ name: user.name, count: unread() }">
117
+ Hi $name, you have $count new messages
118
+ </t>
119
+ ```
120
+
121
+ A translation can move the values to wherever the language needs them, and the
122
+ text around them can contain HTML:
123
+
124
+ ```html
125
+ <t [v]="{ count: cart.items.length, total: cart.total | currency }">
126
+ You have <b>$count items</b> in your cart, totalling <b>$total</b>.
127
+ </t>
128
+ ```
129
+
130
+ When only the values change, the cached translation is reused and no request is
131
+ made.
132
+
133
+ - Only keys of `[v]` are replaced, so text like `$5` stays as it is. If two
134
+ keys start the same way, the longest one wins.
135
+ - Values are inserted as text, never as HTML. Put markup in the text around
136
+ them instead: `Hi <b>$name</b>`.
137
+ - Variables are replaced in text only, not in attributes such as `href`.
138
+ - In development mode, a key that is not found in the text logs a warning.
139
+
140
+ The pipe and `translate` take the values in `v` as well:
141
+
142
+ ```html
143
+ <input
144
+ [placeholder]="'Search $count products' | t: { v: { count: products.length } }"
145
+ />
146
+ ```
147
+
148
+ ```ts
149
+ await magicTranslate.translate('Your order $id has shipped', { v: { id } });
150
+ ```
151
+
108
152
  ### Options per translation
109
153
 
110
154
  Both the component and the pipe can override the target language and the
@@ -116,6 +160,17 @@ formality:
116
160
  <input [placeholder]="'Your name' | t: { lang: 'fr', formality: 'formal' }" />
117
161
  ```
118
162
 
163
+ An `id` lets you edit one use of a text on its own in the webapp, e.g. to
164
+ translate the same "Save" differently on two buttons. Its edited translation
165
+ stays when the text changes. Ids are unique within your account and at most 128
166
+ characters.
167
+
168
+ ```html
169
+ <t id="checkout-save">Save</t>
170
+
171
+ <button [title]="'Save' | t: { id: 'checkout-save' }"></button>
172
+ ```
173
+
119
174
  ### Switching languages
120
175
 
121
176
  Inject the `MagicTranslate` service to read or change the current language.
@@ -169,7 +224,15 @@ const message = await magicTranslate.translate('Your order has been placed');
169
224
  ## Server-side rendering
170
225
 
171
226
  Translations are only loaded in the browser. With SSR the server renders the
172
- original content, and it is translated after hydration.
227
+ original content, and it is translated after hydration. A `<t>` with `[v]`
228
+ renders empty on the server, so `$name` never shows, and appears once the app
229
+ runs in the browser.
230
+
231
+ Code that translates on the server, such as a resolver run while prerendering,
232
+ sends your secret key (`p-...`) from the `MAGIC_TRANSLATE_SECRET_KEY`
233
+ environment variable instead of the API key. Create it on your
234
+ [account page](https://magictranslate.io/account) and keep it out of
235
+ `provideMagicTranslate`, which also runs in the browser.
173
236
 
174
237
  ## License
175
238
 
@@ -1,10 +1,72 @@
1
1
  import { DataLoader, cacheKeyFn, defaultLoaderFactory, libraryName, libraryWebsite } from "@magic-translate/core";
2
2
  import * as i0 from "@angular/core";
3
- import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Injectable, InjectionToken, PLATFORM_ID, Pipe, SecurityContext, ViewEncapsulation, afterNextRender, computed, effect, inject, input, makeEnvironmentProviders, signal, viewChild } from "@angular/core";
3
+ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Injectable, InjectionToken, PLATFORM_ID, Pipe, SecurityContext, ViewEncapsulation, afterNextRender, computed, effect, inject, input, isDevMode, makeEnvironmentProviders, signal, untracked, viewChild } from "@angular/core";
4
4
  import { DomSanitizer } from "@angular/platform-browser";
5
5
  import { isPlatformServer } from "@angular/common";
6
6
  export * from "@magic-translate/core";
7
7
  const MAGIC_TRANSLATE_CONFIG = new InjectionToken("MAGIC_TRANSLATE_CONFIG");
8
+ const PLACEHOLDER = /\{(\d+)\}/g;
9
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
+ function templater(variables) {
11
+ const names = Object.keys(variables).sort((a, b) => b.length - a.length);
12
+ const pattern = names.length > 0 ? new RegExp(`\\$(${names.map(escapeRegExp).join("|")})`, "g") : null;
13
+ const indexes = /* @__PURE__ */ new Map();
14
+ const values = [];
15
+ return {
16
+ values,
17
+ replace(text) {
18
+ if (!pattern) return text;
19
+ return text.replace(pattern, (_, name) => {
20
+ let index = indexes.get(name);
21
+ if (index === void 0) {
22
+ index = values.push(String(variables[name] ?? "")) - 1;
23
+ indexes.set(name, index);
24
+ }
25
+ return `{${index}}`;
26
+ });
27
+ },
28
+ unused: () => names.filter((name) => !indexes.has(name))
29
+ };
30
+ }
31
+ function warnAboutUnused(unused) {
32
+ if (isDevMode() && unused.length > 0) console.warn(`[${libraryName}]: ${unused.map((name) => `$${name}`).join(", ")} not found in the text. Check the spelling, or remove ${unused.length === 1 ? "it" : "them"} from the variables.`);
33
+ }
34
+ function toTemplate(text, variables) {
35
+ const { replace, values, unused } = templater(variables);
36
+ return {
37
+ template: replace(text),
38
+ values,
39
+ unused: unused()
40
+ };
41
+ }
42
+ function fillPlaceholders(text, values) {
43
+ return text.replace(PLACEHOLDER, (placeholder, index) => Number(index) < values.length ? values[Number(index)] : placeholder);
44
+ }
45
+ function textNodes(root) {
46
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
47
+ const nodes = [];
48
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node);
49
+ return nodes;
50
+ }
51
+ function toHtmlTemplate(html, variables) {
52
+ const container = document.createElement("template");
53
+ container.innerHTML = html;
54
+ const { replace, values, unused } = templater(variables);
55
+ textNodes(container.content).forEach((node) => {
56
+ node.data = replace(node.data);
57
+ });
58
+ return {
59
+ template: container.innerHTML,
60
+ values,
61
+ unused: unused()
62
+ };
63
+ }
64
+ function fillElement(element, values) {
65
+ textNodes(element).forEach((node) => {
66
+ const filled = fillPlaceholders(node.data, values);
67
+ if (filled !== node.data) node.data = filled;
68
+ });
69
+ }
8
70
  var MagicTranslate = class MagicTranslate {
9
71
  config = inject(MAGIC_TRANSLATE_CONFIG);
10
72
  languageOverride = signal(null, ...ngDevMode ? [{ debugName: "languageOverride" }] : /* istanbul ignore next */ []);
@@ -12,12 +74,13 @@ var MagicTranslate = class MagicTranslate {
12
74
  sourceLanguage = this.config.sourceLanguage;
13
75
  language = computed(() => this.languageOverride() ?? this.config.language, ...ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []);
14
76
  dataLoader = computed(() => {
15
- const { debug, apiKey, sourceLanguage, _apiUrl, formality } = this.config;
77
+ const { debug, apiKey, secretKey, sourceLanguage, _apiUrl, formality } = this.config;
16
78
  const loaderFactory = this.config._loaderFactory ?? defaultLoaderFactory;
17
79
  const language = this.language();
18
80
  return new DataLoader(loaderFactory({
19
81
  debug: !!debug,
20
82
  apiKey,
83
+ secretKey,
21
84
  lang: language,
22
85
  sourceLang: sourceLanguage,
23
86
  apiUrl: _apiUrl,
@@ -30,11 +93,17 @@ var MagicTranslate = class MagicTranslate {
30
93
  setLanguage(language) {
31
94
  this.languageOverride.set(language);
32
95
  }
33
- translate(text, params = {}) {
34
- return this.dataLoader().load({
96
+ translate(text, { v, ...params } = {}) {
97
+ if (!v) return this.dataLoader().load({
35
98
  text,
36
99
  ...params
37
100
  });
101
+ const { template, values, unused } = toTemplate(text, v);
102
+ warnAboutUnused(unused);
103
+ return this.dataLoader().load({
104
+ text: template,
105
+ ...params
106
+ }).then((translation) => fillPlaceholders(translation, values));
38
107
  }
39
108
  cacheKey(text, params = {}) {
40
109
  return cacheKeyFn(this.apiKey, this.sourceLanguage, this.language())({
@@ -100,6 +169,8 @@ function readSource(element) {
100
169
  var Translate = class Translate {
101
170
  lang = input(void 0, ...ngDevMode ? [{ debugName: "lang" }] : /* istanbul ignore next */ []);
102
171
  formality = input(void 0, ...ngDevMode ? [{ debugName: "formality" }] : /* istanbul ignore next */ []);
172
+ id = input(void 0, ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
173
+ v = input(void 0, ...ngDevMode ? [{ debugName: "v" }] : /* istanbul ignore next */ []);
103
174
  magicTranslate = inject(MagicTranslate);
104
175
  sanitizer = inject(DomSanitizer);
105
176
  sourceRef = viewChild.required("source", ...ngDevMode ? [{ debugName: "sourceRef" }] : /* istanbul ignore next */ []);
@@ -107,6 +178,7 @@ var Translate = class Translate {
107
178
  source = signal(null, ...ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []);
108
179
  translated = signal(false, ...ngDevMode ? [{ debugName: "translated" }] : /* istanbul ignore next */ []);
109
180
  lastRequestId = 0;
181
+ lastTemplate;
110
182
  constructor() {
111
183
  const destroyRef = inject(DestroyRef);
112
184
  afterNextRender(() => {
@@ -126,21 +198,34 @@ var Translate = class Translate {
126
198
  const source = this.source();
127
199
  const lang = this.lang();
128
200
  const formality = this.formality();
201
+ const id = this.id();
202
+ const variables = this.v();
129
203
  this.magicTranslate.language();
130
204
  if (!source?.html) {
131
205
  this.translated.set(false);
132
206
  return;
133
207
  }
208
+ const { template, values, unused } = variables ? toHtmlTemplate(source.html, variables) : {
209
+ template: source.html,
210
+ values: [],
211
+ unused: []
212
+ };
213
+ if (template !== this.lastTemplate) {
214
+ this.lastTemplate = template;
215
+ warnAboutUnused(unused);
216
+ }
217
+ if (variables && !untracked(this.translated)) this.render(source, template, values);
134
218
  const requestId = ++this.lastRequestId;
135
- this.magicTranslate.translate(source.html, {
219
+ this.magicTranslate.translate(template, {
136
220
  lang,
137
- formality
221
+ formality,
222
+ id
138
223
  }).then((translation) => {
139
- if (requestId === this.lastRequestId) this.render(source, translation);
224
+ if (requestId === this.lastRequestId) this.render(source, translation, values);
140
225
  });
141
226
  });
142
227
  }
143
- render(source, translation) {
228
+ render(source, translation, values = []) {
144
229
  const target = this.targetRef().nativeElement;
145
230
  const html = this.sanitizer.sanitize(SecurityContext.HTML, translation);
146
231
  if (html === null) {
@@ -148,6 +233,7 @@ var Translate = class Translate {
148
233
  return;
149
234
  }
150
235
  target.innerHTML = `${source.leadingWhitespace}${html}${source.trailingWhitespace}`;
236
+ if (values.length > 0) fillElement(target, values);
151
237
  if (source.scopeAttribute) target.querySelectorAll("*").forEach((el) => el.setAttribute(source.scopeAttribute, ""));
152
238
  this.translated.set(true);
153
239
  }
@@ -179,9 +265,26 @@ var Translate = class Translate {
179
265
  isSignal: true,
180
266
  isRequired: false,
181
267
  transformFunction: null
268
+ },
269
+ id: {
270
+ classPropertyName: "id",
271
+ publicName: "id",
272
+ isSignal: true,
273
+ isRequired: false,
274
+ transformFunction: null
275
+ },
276
+ v: {
277
+ classPropertyName: "v",
278
+ publicName: "v",
279
+ isSignal: true,
280
+ isRequired: false,
281
+ transformFunction: null
182
282
  }
183
283
  },
184
- host: { styleAttribute: "display: contents" },
284
+ host: {
285
+ properties: { "attr.id": "null" },
286
+ styleAttribute: "display: contents"
287
+ },
185
288
  viewQueries: [{
186
289
  propertyName: "sourceRef",
187
290
  first: true,
@@ -196,7 +299,9 @@ var Translate = class Translate {
196
299
  isSignal: true
197
300
  }],
198
301
  ngImport: i0,
199
- template: `<span #source [style.display]="translated() ? 'none' : 'contents'"
302
+ template: `<span
303
+ #source
304
+ [style.display]="translated() || v() ? 'none' : 'contents'"
200
305
  ><ng-content /></span
201
306
  ><span
202
307
  #target
@@ -216,13 +321,18 @@ i0.ɵɵngDeclareClassMetadata({
216
321
  type: Component,
217
322
  args: [{
218
323
  selector: "t, mt-translate",
219
- template: `<span #source [style.display]="translated() ? 'none' : 'contents'"
324
+ template: `<span
325
+ #source
326
+ [style.display]="translated() || v() ? 'none' : 'contents'"
220
327
  ><ng-content /></span
221
328
  ><span
222
329
  #target
223
330
  [style.display]="translated() ? 'contents' : 'none'"
224
331
  ></span>`,
225
- host: { style: "display: contents" },
332
+ host: {
333
+ style: "display: contents",
334
+ "[attr.id]": "null"
335
+ },
226
336
  encapsulation: ViewEncapsulation.None,
227
337
  changeDetection: ChangeDetectionStrategy.OnPush
228
338
  }]
@@ -245,6 +355,22 @@ i0.ɵɵngDeclareClassMetadata({
245
355
  required: false
246
356
  }]
247
357
  }],
358
+ id: [{
359
+ type: i0.Input,
360
+ args: [{
361
+ isSignal: true,
362
+ alias: "id",
363
+ required: false
364
+ }]
365
+ }],
366
+ v: [{
367
+ type: i0.Input,
368
+ args: [{
369
+ isSignal: true,
370
+ alias: "v",
371
+ required: false
372
+ }]
373
+ }],
248
374
  sourceRef: [{
249
375
  type: i0.ViewChild,
250
376
  args: ["source", { isSignal: true }]
@@ -262,20 +388,26 @@ var TranslatePipe = class TranslatePipe {
262
388
  isServer = isPlatformServer(inject(PLATFORM_ID));
263
389
  key;
264
390
  translation;
265
- transform(text, params = {}) {
266
- if (!text || this.isServer) return text;
267
- const key = this.magicTranslate.cacheKey(text, params);
391
+ transform(text, { v, ...params } = {}) {
392
+ const { template, values, unused } = v ? toTemplate(text, v) : {
393
+ template: text,
394
+ values: [],
395
+ unused: []
396
+ };
397
+ if (!text || this.isServer) return fillPlaceholders(template, values);
398
+ const key = this.magicTranslate.cacheKey(template, params);
268
399
  if (key !== this.key) {
400
+ warnAboutUnused(unused);
269
401
  this.key = key;
270
402
  this.translation = void 0;
271
- this.magicTranslate.translate(text, params).then((translation) => {
403
+ this.magicTranslate.translate(template, params).then((translation) => {
272
404
  if (this.key === key) {
273
405
  this.translation = translation;
274
406
  this.changeDetectorRef.markForCheck();
275
407
  }
276
408
  });
277
409
  }
278
- return this.translation ?? text;
410
+ return fillPlaceholders(this.translation ?? template, values);
279
411
  }
280
412
  static ɵfac = i0.ɵɵngDeclareFactory({
281
413
  minVersion: "12.0.0",
@@ -1 +1 @@
1
- {"version":3,"file":"magic-translate-angular.mjs","names":[],"sources":["../../src/lib/magic-translate-config.ts","../../src/lib/magic-translate.service.ts","../../src/lib/provide-magic-translate.ts","../../src/lib/translate.component.ts","../../src/lib/translate.pipe.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { MagicTranslateConfig } from '@magic-translate/core';\n\nexport const MAGIC_TRANSLATE_CONFIG = new InjectionToken<MagicTranslateConfig>(\n 'MAGIC_TRANSLATE_CONFIG'\n);\n","import { computed, inject, Injectable, signal } from '@angular/core';\nimport {\n cacheKeyFn,\n DataLoader,\n DataLoaderLoadParams,\n defaultLoaderFactory,\n Language,\n libraryWebsite,\n} from '@magic-translate/core';\nimport { MAGIC_TRANSLATE_CONFIG } from './magic-translate-config';\n\n/**\n * Type alias to make the language enum more flexible to also allow passing language strings\n * instead of just the enum.\n */\nexport type Languages = `${Language}`;\n\nexport type TranslateParams = Omit<DataLoaderLoadParams, 'text'>;\n\n@Injectable()\nexport class MagicTranslate {\n private readonly config = inject(MAGIC_TRANSLATE_CONFIG);\n private readonly languageOverride = signal<Languages | null>(null);\n\n readonly apiKey = this.config.apiKey;\n readonly sourceLanguage = this.config.sourceLanguage;\n\n /**\n * The language content is currently translated to.\n */\n readonly language = computed(\n () => this.languageOverride() ?? this.config.language\n );\n\n readonly dataLoader = computed(() => {\n const { debug, apiKey, sourceLanguage, _apiUrl, formality } = this.config;\n const loaderFactory = this.config._loaderFactory ?? defaultLoaderFactory;\n const language = this.language();\n\n return new DataLoader<DataLoaderLoadParams, string, string>(\n loaderFactory({\n debug: !!debug,\n apiKey,\n lang: language,\n sourceLang: sourceLanguage,\n apiUrl: _apiUrl,\n formality,\n }),\n { cacheKeyFn: cacheKeyFn(apiKey, sourceLanguage, language) }\n );\n });\n\n constructor() {\n if (!this.config.apiKey) {\n throw new Error(`Missing API key. Please get one at ${libraryWebsite}`);\n }\n }\n\n setLanguage(language: Languages): void {\n this.languageOverride.set(language);\n }\n\n translate(text: string, params: TranslateParams = {}): Promise<string> {\n return this.dataLoader().load({ text, ...params });\n }\n\n /** @ignore */\n cacheKey(text: string, params: TranslateParams = {}): string {\n return cacheKeyFn(\n this.apiKey,\n this.sourceLanguage,\n this.language()\n )({ text, ...params });\n }\n}\n","import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';\nimport { MagicTranslateConfig } from '@magic-translate/core';\nimport { MAGIC_TRANSLATE_CONFIG } from './magic-translate-config';\nimport { MagicTranslate } from './magic-translate.service';\n\n/**\n * Sets up Magic Translate for the application. Add it to the `providers` of\n * your application config.\n */\nexport function provideMagicTranslate(\n config: MagicTranslateConfig\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: MAGIC_TRANSLATE_CONFIG, useValue: config },\n MagicTranslate,\n ]);\n}\n","import {\n afterNextRender,\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n effect,\n ElementRef,\n inject,\n input,\n SecurityContext,\n signal,\n viewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { libraryName, TranslateOptions } from '@magic-translate/core';\nimport { Languages, MagicTranslate } from './magic-translate.service';\n\ninterface Source {\n html: string;\n leadingWhitespace: string;\n trailingWhitespace: string;\n // The `_ngcontent-*` attribute of the host template, so its component\n // styles still apply to the translated markup.\n scopeAttribute?: string;\n}\n\n/**\n * Turns the projected content into the HTML string that gets translated,\n * without the comments and attributes Angular adds to the DOM.\n */\nfunction readSource(element: HTMLElement): Source {\n const clone = element.cloneNode(true) as HTMLElement;\n const comments: Node[] = [];\n let scopeAttribute: string | undefined;\n\n const walker = document.createTreeWalker(\n clone,\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT\n );\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === Node.COMMENT_NODE) {\n comments.push(node);\n continue;\n }\n\n const el = node as Element;\n if (el.localName.includes('-')) {\n throw new Error(\n `Components are not allowed inside \\`<t>...</t>\\` components. The component found inside the t component is <${el.localName}>.`\n );\n }\n\n for (const { name } of Array.from(el.attributes)) {\n if (name.startsWith('_ngcontent-')) {\n scopeAttribute = name;\n }\n if (name.startsWith('_ng') || name.startsWith('ng-reflect-')) {\n el.removeAttribute(name);\n }\n }\n }\n comments.forEach((comment) => comment.parentNode?.removeChild(comment));\n\n const [, leadingWhitespace, html, trailingWhitespace] =\n /^(\\s*)([\\s\\S]*?)(\\s*)$/.exec(clone.innerHTML)!;\n\n return { html, leadingWhitespace, trailingWhitespace, scopeAttribute };\n}\n\n@Component({\n selector: 't, mt-translate',\n template: `<span #source [style.display]=\"translated() ? 'none' : 'contents'\"\n ><ng-content /></span\n ><span\n #target\n [style.display]=\"translated() ? 'contents' : 'none'\"\n ></span>`,\n host: { style: 'display: contents' },\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class Translate {\n readonly lang = input<Languages | undefined>(undefined);\n readonly formality = input<TranslateOptions['formality']>(undefined);\n\n private readonly magicTranslate = inject(MagicTranslate);\n private readonly sanitizer = inject(DomSanitizer);\n private readonly sourceRef =\n viewChild.required<ElementRef<HTMLElement>>('source');\n private readonly targetRef =\n viewChild.required<ElementRef<HTMLElement>>('target');\n\n private readonly source = signal<Source | null>(null);\n protected readonly translated = signal(false);\n private lastRequestId = 0;\n\n constructor() {\n const destroyRef = inject(DestroyRef);\n\n afterNextRender(() => {\n const element = this.sourceRef().nativeElement;\n const read = () => this.source.set(readSource(element));\n read();\n\n // Picks up changes to the projected content, such as interpolations.\n const observer = new MutationObserver(read);\n observer.observe(element, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n });\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n effect(() => {\n const source = this.source();\n const lang = this.lang();\n const formality = this.formality();\n // a language switch has to re-translate the content\n this.magicTranslate.language();\n\n if (!source?.html) {\n this.translated.set(false);\n return;\n }\n\n // make sure that we only ever use the response of the most recent request.\n const requestId = ++this.lastRequestId;\n this.magicTranslate\n .translate(source.html, { lang, formality })\n .then((translation) => {\n if (requestId === this.lastRequestId) {\n this.render(source, translation);\n }\n });\n });\n }\n\n private render(source: Source, translation: string) {\n const target = this.targetRef().nativeElement;\n const html = this.sanitizer.sanitize(SecurityContext.HTML, translation);\n if (html === null) {\n console.warn(`[${libraryName}]: Could not render translation.`);\n return;\n }\n\n target.innerHTML = `${source.leadingWhitespace}${html}${source.trailingWhitespace}`;\n if (source.scopeAttribute) {\n target\n .querySelectorAll('*')\n .forEach((el) => el.setAttribute(source.scopeAttribute!, ''));\n }\n this.translated.set(true);\n }\n}\n\nexport const T = Translate;\n","import { isPlatformServer } from '@angular/common';\nimport {\n ChangeDetectorRef,\n inject,\n Pipe,\n PipeTransform,\n PLATFORM_ID,\n} from '@angular/core';\nimport { MagicTranslate, TranslateParams } from './magic-translate.service';\n\n/**\n * Translates a plain string, e.g. for attributes:\n * `<input [placeholder]=\"'Search' | t\" />`. Returns the original text until\n * the translation has loaded.\n */\n@Pipe({ name: 't', pure: false })\nexport class TranslatePipe implements PipeTransform {\n private readonly magicTranslate = inject(MagicTranslate);\n private readonly changeDetectorRef = inject(ChangeDetectorRef);\n private readonly isServer = isPlatformServer(inject(PLATFORM_ID));\n\n private key?: string;\n private translation?: string;\n\n transform(text: string, params: TranslateParams = {}): string {\n if (!text || this.isServer) {\n return text;\n }\n\n const key = this.magicTranslate.cacheKey(text, params);\n if (key !== this.key) {\n this.key = key;\n this.translation = undefined;\n this.magicTranslate.translate(text, params).then((translation) => {\n if (this.key === key) {\n this.translation = translation;\n this.changeDetectorRef.markForCheck();\n }\n });\n }\n\n return this.translation ?? text;\n }\n}\n"],"mappings":";;;;;;AAGA,MAAa,yBAAyB,IAAI,eACxC,wBAAwB;ACgB1B,IAAa,iBAAb,MAAa,eAAc;CACR,SAAS,OAAO,sBAAsB;CACtC,mBAAmB,OAAyB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAEzD,SAAS,KAAK,OAAO;CACrB,iBAAiB,KAAK,OAAO;CAK7B,WAAW,eACZ,KAAK,iBAAgB,KAAM,KAAK,OAAO,UAC9C,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAEQ,aAAa,eAAc;EAClC,MAAM,EAAE,OAAO,QAAQ,gBAAgB,SAAS,cAAc,KAAK;EACnE,MAAM,gBAAgB,KAAK,OAAO,kBAAkB;EACpD,MAAM,WAAW,KAAK,SAAQ;EAE9B,OAAO,IAAI,WACT,cAAc;GACZ,OAAO,CAAC,CAAC;GACT;GACA,MAAM;GACN,YAAY;GACZ,QAAQ;GACR;EACD,CAAA,GACD,EAAE,YAAY,WAAW,QAAQ,gBAAgB,QAAQ,EAAC,CAAE;CAEhE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEF,cAAA;EACE,IAAI,CAAC,KAAK,OAAO,QACf,MAAM,IAAI,MAAM,sCAAsC,gBAAgB;CAE1E;CAEA,YAAY,UAAmB;EAC7B,KAAK,iBAAiB,IAAI,QAAQ;CACpC;CAEA,UAAU,MAAc,SAA0B,CAAA,GAAE;EAClD,OAAO,KAAK,WAAU,CAAE,CAAC,KAAK;GAAE;GAAM,GAAG;EAAM,CAAE;CACnD;CAGA,SAAS,MAAc,SAA0B,CAAA,GAAE;EACjD,OAAO,WACL,KAAK,QACL,KAAK,gBACL,KAAK,SAAQ,CAAE,CAChB,CAAC;GAAE;GAAM,GAAG;EAAM,CAAE;CACvB;CArDW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAc,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAd,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAc,CAAA;;AAAd,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAc,YAAA,CAAA,EAD1B,MAAA,W;;;ACVD,SAAgB,sBACd,QAA4B;CAE5B,OAAO,yBAAyB,CAC9B;EAAE,SAAS;EAAwB,UAAU;CAAM,GACnD,cACD,CAAA;AACH;ACeA,SAAS,WAAW,SAAoB;CACtC,MAAM,QAAQ,QAAQ,UAAU,IAAI;CACpC,MAAM,WAAmB,CAAA;CACzB,IAAI;CAEJ,MAAM,SAAS,SAAS,iBACtB,OACA,WAAW,eAAe,WAAW,YAAY;CAEnD,KAAK,IAAI,OAAO,OAAO,SAAQ,GAAI,MAAM,OAAO,OAAO,SAAQ,GAAI;EACjE,IAAI,KAAK,aAAa,KAAK,cAAc;GACvC,SAAS,KAAK,IAAI;GAClB;EACF;EAEA,MAAM,KAAK;EACX,IAAI,GAAG,UAAU,SAAS,GAAG,GAC3B,MAAM,IAAI,MACR,+GAA+G,GAAG,UAAS,GAAI;EAInI,KAAK,MAAM,EAAE,UAAU,MAAM,KAAK,GAAG,UAAU,GAAG;GAChD,IAAI,KAAK,WAAW,aAAa,GAC/B,iBAAiB;GAEnB,IAAI,KAAK,WAAW,KAAK,KAAK,KAAK,WAAW,aAAa,GACzD,GAAG,gBAAgB,IAAI;EAE3B;CACF;CACA,SAAS,SAAS,YAAY,QAAQ,YAAY,YAAY,OAAO,CAAC;CAEtE,MAAM,GAAG,mBAAmB,MAAM,sBAChC,yBAAyB,KAAK,MAAM,SAAS;CAE/C,OAAO;EAAE;EAAM;EAAmB;EAAoB;CAAc;AACtE;AAcA,IAAa,YAAb,MAAa,UAAS;CACX,OAAO,MAA6B,QAAU,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAC9C,YAAY,MAAqC,QAAU,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEnD,iBAAiB,OAAO,cAAc;CACtC,YAAY,OAAO,YAAY;CAC/B,YACf,UAAU,SAAkC,UAAS,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CACtC,YACf,UAAU,SAAkC,UAAS,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEtC,SAAS,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAClC,aAAa,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CACrC,gBAAgB;CAExB,cAAA;EACE,MAAM,aAAa,OAAO,UAAU;EAEpC,sBAAqB;GACnB,MAAM,UAAU,KAAK,UAAS,CAAE,CAAC;GACjC,MAAM,aAAa,KAAK,OAAO,IAAI,WAAW,OAAO,CAAC;GACtD,KAAI;GAGJ,MAAM,WAAW,IAAI,iBAAiB,IAAI;GAC1C,SAAS,QAAQ,SAAS;IACxB,SAAS;IACT,WAAW;IACX,eAAe;IACf,YAAY;GACb,CAAA;GACD,WAAW,gBAAgB,SAAS,WAAU,CAAE;EAClD,CAAC;EAED,aAAY;GACV,MAAM,SAAS,KAAK,OAAM;GAC1B,MAAM,OAAO,KAAK,KAAI;GACtB,MAAM,YAAY,KAAK,UAAS;GAEhC,KAAK,eAAe,SAAQ;GAE5B,IAAI,CAAC,QAAQ,MAAM;IACjB,KAAK,WAAW,IAAI,KAAK;IACzB;GACF;GAGA,MAAM,YAAY,EAAE,KAAK;GACzB,KAAK,eACF,UAAU,OAAO,MAAM;IAAE;IAAM;GAAS,CAAE,CAAC,CAC3C,MAAM,gBAAe;IACpB,IAAI,cAAc,KAAK,eACrB,KAAK,OAAO,QAAQ,WAAW;GAEnC,CAAC;EACL,CAAC;CACH;CAEQ,OAAO,QAAgB,aAAmB;EAChD,MAAM,SAAS,KAAK,UAAS,CAAE,CAAC;EAChC,MAAM,OAAO,KAAK,UAAU,SAAS,gBAAgB,MAAM,WAAW;EACtE,IAAI,SAAS,MAAM;GACjB,QAAQ,KAAK,IAAI,YAAW,iCAAkC;GAC9D;EACF;EAEA,OAAO,YAAY,GAAG,OAAO,oBAAoB,OAAO,OAAO;EAC/D,IAAI,OAAO,gBACT,OACG,iBAAiB,GAAG,CAAC,CACrB,SAAS,OAAO,GAAG,aAAa,OAAO,gBAAiB,EAAE,CAAC;EAEhE,KAAK,WAAW,IAAI,IAAI;CAC1B;CAzEW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAS,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAT,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAS,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,MAAA,EAAA,gBAAA,oBAAA;EAAA,aAAA,CAAA;GAAA,cAAA;GAAA,OAAA;GAAA,WAAA,CAAA,QAAA;GAAA,aAAA;GAAA,UAAA;EAAA,GAAA;GAAA,cAAA;GAAA,OAAA;GAAA,WAAA,CAAA,QAAA;GAAA,aAAA;GAAA,UAAA;EAAA,CAAA;EAAA,UAAA;EAAA,UAVV;;;;;;EAKC,UAAA;EAAA,iBAAA,GAAA,wBAAA;EAAA,eAAA,GAAA,kBAAA;CAAA,CAAA;;AAKA,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAS,YAAA,CAAA;EAZrB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,UAAU;;;;;;GAMV,MAAM,EAAE,OAAO,oBAAmB;GAClC,eAAe,kBAAkB;GACjC,iBAAiB,wBAAwB;EAC1C,CAAA;CAQ+C,CAAA;CAAA,sBAAA,CAAA;CAAA,gBAAA;EAAA,MAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,UAAQ,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAER,UAAQ,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;CAAA;AAAA,CAAA;AAmExD,MAAa,IAAI;AC9IjB,IAAa,gBAAb,MAAa,cAAa;CACP,iBAAiB,OAAO,cAAc;CACtC,oBAAoB,OAAO,iBAAiB;CAC5C,WAAW,iBAAiB,OAAO,WAAW,CAAC;CAExD;CACA;CAER,UAAU,MAAc,SAA0B,CAAA,GAAE;EAClD,IAAI,CAAC,QAAQ,KAAK,UAChB,OAAO;EAGT,MAAM,MAAM,KAAK,eAAe,SAAS,MAAM,MAAM;EACrD,IAAI,QAAQ,KAAK,KAAK;GACpB,KAAK,MAAM;GACX,KAAK,cAAc;GACnB,KAAK,eAAe,UAAU,MAAM,MAAM,CAAC,CAAC,MAAM,gBAAe;IAC/D,IAAI,KAAK,QAAQ,KAAK;KACpB,KAAK,cAAc;KACnB,KAAK,kBAAkB,aAAY;IACrC;GACF,CAAC;EACH;EAEA,OAAO,KAAK,eAAe;CAC7B;CA1BW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAa,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAb,OAAA,QAAA,GAAA,gBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAa,cAAA;EAAA,MAAA;EAAA,MAAA;CAAA,CAAA;;AAAb,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAa,YAAA,CAAA;EADzB,MAAA;EAAK,MAAA,CAAA;GAAE,MAAM;GAAK,MAAM;EAAK,CAAE"}
1
+ {"version":3,"file":"magic-translate-angular.mjs","names":[],"sources":["../../src/lib/magic-translate-config.ts","../../src/lib/variables.ts","../../src/lib/magic-translate.service.ts","../../src/lib/provide-magic-translate.ts","../../src/lib/translate.component.ts","../../src/lib/translate.pipe.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { MagicTranslateConfig } from '@magic-translate/core';\n\nexport const MAGIC_TRANSLATE_CONFIG = new InjectionToken<MagicTranslateConfig>(\n 'MAGIC_TRANSLATE_CONFIG'\n);\n","import { isDevMode } from '@angular/core';\nimport { libraryName } from '@magic-translate/core';\n\n/**\n * Values that are not translated, referenced as `$name` in the text.\n */\nexport type Variables = Record<string, unknown>;\n\nconst PLACEHOLDER = /\\{(\\d+)\\}/g;\n\nconst escapeRegExp = (text: string) =>\n text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Replaces `$name` with numbered placeholders, `{0}`, `{1}`…, in the order the\n * names first appear, so the API sees the same template as from the React\n * client. Only names that are keys of `variables` are replaced, and the longest\n * key wins, so `$counts` is not read as `$count` followed by \"s\".\n *\n * `replace` can be called for several texts, e.g. every text node of an HTML\n * fragment, and numbers the placeholders across all of them.\n */\nexport function templater(variables: Variables) {\n const names = Object.keys(variables).sort((a, b) => b.length - a.length);\n const pattern =\n names.length > 0\n ? new RegExp(`\\\\$(${names.map(escapeRegExp).join('|')})`, 'g')\n : null;\n const indexes = new Map<string, number>();\n const values: string[] = [];\n\n return {\n values,\n replace(text: string): string {\n if (!pattern) {\n return text;\n }\n return text.replace(pattern, (_, name: string) => {\n let index = indexes.get(name);\n if (index === undefined) {\n index = values.push(String(variables[name] ?? '')) - 1;\n indexes.set(name, index);\n }\n return `{${index}}`;\n });\n },\n unused: () => names.filter((name) => !indexes.has(name)),\n };\n}\n\nexport interface Template {\n template: string;\n values: string[];\n // names that are not in the text, most likely misspelled\n unused: string[];\n}\n\nexport function warnAboutUnused(unused: readonly string[]) {\n if (isDevMode() && unused.length > 0) {\n console.warn(\n `[${libraryName}]: ${unused.map((name) => `$${name}`).join(', ')} not found in the text. Check the spelling, or remove ${unused.length === 1 ? 'it' : 'them'} from the variables.`\n );\n }\n}\n\nexport function toTemplate(text: string, variables: Variables): Template {\n const { replace, values, unused } = templater(variables);\n return { template: replace(text), values, unused: unused() };\n}\n\n/**\n * Puts the values back into a translated text. A placeholder without a value\n * is left as it is.\n */\nexport function fillPlaceholders(text: string, values: readonly string[]) {\n return text.replace(PLACEHOLDER, (placeholder, index: string) =>\n Number(index) < values.length ? values[Number(index)] : placeholder\n );\n}\n\nfunction textNodes(root: Node): Text[] {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n const nodes: Text[] = [];\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n nodes.push(node as Text);\n }\n return nodes;\n}\n\n/**\n * Like {@link toTemplate}, for HTML. Only text is replaced, never attributes,\n * because only text can be filled in again.\n */\nexport function toHtmlTemplate(html: string, variables: Variables): Template {\n const container = document.createElement('template');\n container.innerHTML = html;\n const { replace, values, unused } = templater(variables);\n textNodes(container.content).forEach((node) => {\n node.data = replace(node.data);\n });\n return { template: container.innerHTML, values, unused: unused() };\n}\n\n/**\n * Fills the placeholders in the text of an element. The values are set as\n * text, so they are never parsed as HTML.\n */\nexport function fillElement(element: Element, values: readonly string[]) {\n textNodes(element).forEach((node) => {\n const filled = fillPlaceholders(node.data, values);\n if (filled !== node.data) {\n node.data = filled;\n }\n });\n}\n","import { computed, inject, Injectable, signal } from '@angular/core';\nimport {\n cacheKeyFn,\n DataLoader,\n DataLoaderLoadParams,\n defaultLoaderFactory,\n Language,\n libraryWebsite,\n} from '@magic-translate/core';\nimport { MAGIC_TRANSLATE_CONFIG } from './magic-translate-config';\nimport {\n fillPlaceholders,\n toTemplate,\n Variables,\n warnAboutUnused,\n} from './variables';\n\n/**\n * Type alias to make the language enum more flexible to also allow passing language strings\n * instead of just the enum.\n */\nexport type Languages = `${Language}`;\n\nexport interface TranslateParams extends Omit<DataLoaderLoadParams, 'text'> {\n /**\n * Values that are not translated, referenced as `$name` in the text. The text\n * is translated once as a template and cached, whatever the values are.\n */\n v?: Variables;\n}\n\n@Injectable()\nexport class MagicTranslate {\n private readonly config = inject(MAGIC_TRANSLATE_CONFIG);\n private readonly languageOverride = signal<Languages | null>(null);\n\n readonly apiKey = this.config.apiKey;\n readonly sourceLanguage = this.config.sourceLanguage;\n\n /**\n * The language content is currently translated to.\n */\n readonly language = computed(\n () => this.languageOverride() ?? this.config.language\n );\n\n readonly dataLoader = computed(() => {\n const { debug, apiKey, secretKey, sourceLanguage, _apiUrl, formality } =\n this.config;\n const loaderFactory = this.config._loaderFactory ?? defaultLoaderFactory;\n const language = this.language();\n\n return new DataLoader<DataLoaderLoadParams, string, string>(\n loaderFactory({\n debug: !!debug,\n apiKey,\n secretKey,\n lang: language,\n sourceLang: sourceLanguage,\n apiUrl: _apiUrl,\n formality,\n }),\n { cacheKeyFn: cacheKeyFn(apiKey, sourceLanguage, language) }\n );\n });\n\n constructor() {\n if (!this.config.apiKey) {\n throw new Error(`Missing API key. Please get one at ${libraryWebsite}`);\n }\n }\n\n setLanguage(language: Languages): void {\n this.languageOverride.set(language);\n }\n\n translate(\n text: string,\n { v, ...params }: TranslateParams = {}\n ): Promise<string> {\n if (!v) {\n return this.dataLoader().load({ text, ...params });\n }\n const { template, values, unused } = toTemplate(text, v);\n warnAboutUnused(unused);\n return this.dataLoader()\n .load({ text: template, ...params })\n .then((translation) => fillPlaceholders(translation, values));\n }\n\n /** @ignore */\n cacheKey(text: string, params: Omit<TranslateParams, 'v'> = {}): string {\n return cacheKeyFn(\n this.apiKey,\n this.sourceLanguage,\n this.language()\n )({ text, ...params });\n }\n}\n","import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';\nimport { MagicTranslateConfig } from '@magic-translate/core';\nimport { MAGIC_TRANSLATE_CONFIG } from './magic-translate-config';\nimport { MagicTranslate } from './magic-translate.service';\n\n/**\n * Sets up Magic Translate for the application. Add it to the `providers` of\n * your application config.\n */\nexport function provideMagicTranslate(\n config: MagicTranslateConfig\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: MAGIC_TRANSLATE_CONFIG, useValue: config },\n MagicTranslate,\n ]);\n}\n","import {\n afterNextRender,\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n effect,\n ElementRef,\n inject,\n input,\n SecurityContext,\n signal,\n untracked,\n viewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { libraryName, TranslateOptions } from '@magic-translate/core';\nimport { Languages, MagicTranslate } from './magic-translate.service';\nimport {\n fillElement,\n toHtmlTemplate,\n Variables,\n warnAboutUnused,\n} from './variables';\n\ninterface Source {\n html: string;\n leadingWhitespace: string;\n trailingWhitespace: string;\n // The `_ngcontent-*` attribute of the host template, so its component\n // styles still apply to the translated markup.\n scopeAttribute?: string;\n}\n\n/**\n * Turns the projected content into the HTML string that gets translated,\n * without the comments and attributes Angular adds to the DOM.\n */\nfunction readSource(element: HTMLElement): Source {\n const clone = element.cloneNode(true) as HTMLElement;\n const comments: Node[] = [];\n let scopeAttribute: string | undefined;\n\n const walker = document.createTreeWalker(\n clone,\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT\n );\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === Node.COMMENT_NODE) {\n comments.push(node);\n continue;\n }\n\n const el = node as Element;\n if (el.localName.includes('-')) {\n throw new Error(\n `Components are not allowed inside \\`<t>...</t>\\` components. The component found inside the t component is <${el.localName}>.`\n );\n }\n\n for (const { name } of Array.from(el.attributes)) {\n if (name.startsWith('_ngcontent-')) {\n scopeAttribute = name;\n }\n if (name.startsWith('_ng') || name.startsWith('ng-reflect-')) {\n el.removeAttribute(name);\n }\n }\n }\n comments.forEach((comment) => comment.parentNode?.removeChild(comment));\n\n const [, leadingWhitespace, html, trailingWhitespace] =\n /^(\\s*)([\\s\\S]*?)(\\s*)$/.exec(clone.innerHTML)!;\n\n return { html, leadingWhitespace, trailingWhitespace, scopeAttribute };\n}\n\n@Component({\n selector: 't, mt-translate',\n // With variables, the source would show `$name`, so it stays hidden and the\n // text is shown with the values filled in until the translation arrives.\n template: `<span\n #source\n [style.display]=\"translated() || v() ? 'none' : 'contents'\"\n ><ng-content /></span\n ><span\n #target\n [style.display]=\"translated() ? 'contents' : 'none'\"\n ></span>`,\n // `id` is an input, not the element's id, which could clash with the page's.\n host: { style: 'display: contents', '[attr.id]': 'null' },\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class Translate {\n readonly lang = input<Languages | undefined>(undefined);\n readonly formality = input<TranslateOptions['formality']>(undefined);\n /**\n * Lets this use of the text be edited on its own in the webapp. Its edited\n * translation stays when the text changes.\n */\n readonly id = input<string | undefined>(undefined);\n /**\n * Values that are not translated, referenced as `$name` in the content. The\n * text is translated once as a template and cached, whatever the values are.\n */\n readonly v = input<Variables | undefined>(undefined);\n\n private readonly magicTranslate = inject(MagicTranslate);\n private readonly sanitizer = inject(DomSanitizer);\n private readonly sourceRef =\n viewChild.required<ElementRef<HTMLElement>>('source');\n private readonly targetRef =\n viewChild.required<ElementRef<HTMLElement>>('target');\n\n private readonly source = signal<Source | null>(null);\n protected readonly translated = signal(false);\n private lastRequestId = 0;\n private lastTemplate?: string;\n\n constructor() {\n const destroyRef = inject(DestroyRef);\n\n afterNextRender(() => {\n const element = this.sourceRef().nativeElement;\n const read = () => this.source.set(readSource(element));\n read();\n\n // Picks up changes to the projected content, such as interpolations.\n const observer = new MutationObserver(read);\n observer.observe(element, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n });\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n effect(() => {\n const source = this.source();\n const lang = this.lang();\n const formality = this.formality();\n const id = this.id();\n const variables = this.v();\n // a language switch has to re-translate the content\n this.magicTranslate.language();\n\n if (!source?.html) {\n this.translated.set(false);\n return;\n }\n\n const { template, values, unused } = variables\n ? toHtmlTemplate(source.html, variables)\n : { template: source.html, values: [], unused: [] };\n if (template !== this.lastTemplate) {\n this.lastTemplate = template;\n warnAboutUnused(unused);\n }\n if (variables && !untracked(this.translated)) {\n this.render(source, template, values);\n }\n\n // make sure that we only ever use the response of the most recent request.\n const requestId = ++this.lastRequestId;\n this.magicTranslate\n .translate(template, { lang, formality, id })\n .then((translation) => {\n if (requestId === this.lastRequestId) {\n this.render(source, translation, values);\n }\n });\n });\n }\n\n private render(\n source: Source,\n translation: string,\n values: readonly string[] = []\n ) {\n const target = this.targetRef().nativeElement;\n const html = this.sanitizer.sanitize(SecurityContext.HTML, translation);\n if (html === null) {\n console.warn(`[${libraryName}]: Could not render translation.`);\n return;\n }\n\n target.innerHTML = `${source.leadingWhitespace}${html}${source.trailingWhitespace}`;\n if (values.length > 0) {\n fillElement(target, values);\n }\n if (source.scopeAttribute) {\n target\n .querySelectorAll('*')\n .forEach((el) => el.setAttribute(source.scopeAttribute!, ''));\n }\n this.translated.set(true);\n }\n}\n\nexport const T = Translate;\n","import { isPlatformServer } from '@angular/common';\nimport {\n ChangeDetectorRef,\n inject,\n Pipe,\n PipeTransform,\n PLATFORM_ID,\n} from '@angular/core';\nimport { MagicTranslate, TranslateParams } from './magic-translate.service';\nimport { fillPlaceholders, toTemplate, warnAboutUnused } from './variables';\n\n/**\n * Translates a plain string, e.g. for attributes:\n * `<input [placeholder]=\"'Search' | t\" />`. Returns the original text until\n * the translation has loaded.\n *\n * Values that should not be translated go in `v` and are referenced as `$name`:\n * `'Search $count products' | t: { v: { count } }`.\n */\n@Pipe({ name: 't', pure: false })\nexport class TranslatePipe implements PipeTransform {\n private readonly magicTranslate = inject(MagicTranslate);\n private readonly changeDetectorRef = inject(ChangeDetectorRef);\n private readonly isServer = isPlatformServer(inject(PLATFORM_ID));\n\n private key?: string;\n private translation?: string;\n\n transform(text: string, { v, ...params }: TranslateParams = {}): string {\n // Only the template is translated, so new values reuse the translation.\n const { template, values, unused } = v\n ? toTemplate(text, v)\n : { template: text, values: [], unused: [] };\n\n if (!text || this.isServer) {\n return fillPlaceholders(template, values);\n }\n\n const key = this.magicTranslate.cacheKey(template, params);\n if (key !== this.key) {\n warnAboutUnused(unused);\n this.key = key;\n this.translation = undefined;\n this.magicTranslate.translate(template, params).then((translation) => {\n if (this.key === key) {\n this.translation = translation;\n this.changeDetectorRef.markForCheck();\n }\n });\n }\n\n return fillPlaceholders(this.translation ?? template, values);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-translate/angular",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "angular-i18n",
@@ -35,7 +35,7 @@
35
35
  "@angular/platform-browser": ">=20.0.0"
36
36
  },
37
37
  "dependencies": {
38
- "@magic-translate/core": "^1.4.2",
38
+ "@magic-translate/core": "^1.6.0",
39
39
  "tslib": "^2.8.1"
40
40
  },
41
41
  "publishConfig": {
@@ -8,33 +8,89 @@ export declare const MAGIC_TRANSLATE_CONFIG: InjectionToken<MagicTranslateConfig
8
8
  * your application config.
9
9
  */
10
10
  export declare function provideMagicTranslate(config: MagicTranslateConfig): EnvironmentProviders;
11
+ /**
12
+ * Values that are not translated, referenced as `$name` in the text.
13
+ */
14
+ type Variables = Record<string, unknown>;
15
+ /**
16
+ * Replaces `$name` with numbered placeholders, `{0}`, `{1}`…, in the order the
17
+ * names first appear, so the API sees the same template as from the React
18
+ * client. Only names that are keys of `variables` are replaced, and the longest
19
+ * key wins, so `$counts` is not read as `$count` followed by "s".
20
+ *
21
+ * `replace` can be called for several texts, e.g. every text node of an HTML
22
+ * fragment, and numbers the placeholders across all of them.
23
+ */
24
+ declare function templater(variables: Variables): {
25
+ values: string[];
26
+ replace(text: string): string;
27
+ unused: () => string[];
28
+ };
29
+ interface Template {
30
+ template: string;
31
+ values: string[];
32
+ unused: string[];
33
+ }
34
+ declare function warnAboutUnused(unused: readonly string[]): void;
35
+ declare function toTemplate(text: string, variables: Variables): Template;
36
+ /**
37
+ * Puts the values back into a translated text. A placeholder without a value
38
+ * is left as it is.
39
+ */
40
+ declare function fillPlaceholders(text: string, values: readonly string[]): string;
41
+ /**
42
+ * Like {@link toTemplate}, for HTML. Only text is replaced, never attributes,
43
+ * because only text can be filled in again.
44
+ */
45
+ declare function toHtmlTemplate(html: string, variables: Variables): Template;
46
+ /**
47
+ * Fills the placeholders in the text of an element. The values are set as
48
+ * text, so they are never parsed as HTML.
49
+ */
50
+ declare function fillElement(element: Element, values: readonly string[]): void;
11
51
  /**
12
52
  * Type alias to make the language enum more flexible to also allow passing language strings
13
53
  * instead of just the enum.
14
54
  */
15
55
  export type Languages = `${Language}`;
16
- export type TranslateParams = Omit<DataLoaderLoadParams, 'text'>;
56
+ export interface TranslateParams extends Omit<DataLoaderLoadParams, 'text'> {
57
+ /**
58
+ * Values that are not translated, referenced as `$name` in the text. The text
59
+ * is translated once as a template and cached, whatever the values are.
60
+ */
61
+ v?: Variables;
62
+ }
17
63
  export declare class MagicTranslate {
18
64
  private readonly config;
19
65
  private readonly languageOverride;
20
66
  readonly apiKey: string;
21
- readonly sourceLanguage: "af" | "sq" | "am" | "ar" | "hy" | "az" | "bn" | "bs" | "bg" | "ca" | "zh" | "zh-TW" | "hr" | "cs" | "da" | "fa-AF" | "nl" | "en" | "et" | "fa" | "tl" | "fi" | "fr" | "fr-CA" | "ka" | "de" | "el" | "gu" | "ht" | "ha" | "he" | "hi" | "hu" | "is" | "id" | "ga" | "it" | "ja" | "kn" | "kk" | "ko" | "lv" | "lt" | "mk" | "ms" | "ml" | "mt" | "mr" | "mn" | "no" | "ps" | "pl" | "pt" | "pt-PT" | "pa" | "ro" | "ru" | "sr" | "si" | "sk" | "sl" | "so" | "es" | "es-MX" | "sw" | "sv" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "uz" | "vi" | "cy" | undefined;
67
+ readonly sourceLanguage: "it" | "hr" | "th" | "tr" | "af" | "sq" | "am" | "ar" | "hy" | "az" | "bn" | "bs" | "bg" | "ca" | "zh" | "zh-TW" | "cs" | "da" | "fa-AF" | "nl" | "en" | "et" | "fa" | "tl" | "fi" | "fr" | "fr-CA" | "ka" | "de" | "el" | "gu" | "ht" | "ha" | "he" | "hi" | "hu" | "is" | "id" | "ga" | "ja" | "kn" | "kk" | "ko" | "lv" | "lt" | "mk" | "ms" | "ml" | "mt" | "mr" | "mn" | "no" | "ps" | "pl" | "pt" | "pt-PT" | "pa" | "ro" | "ru" | "sr" | "si" | "sk" | "sl" | "so" | "es" | "es-MX" | "sw" | "sv" | "ta" | "te" | "uk" | "ur" | "uz" | "vi" | "cy" | undefined;
22
68
  /**
23
69
  * The language content is currently translated to.
24
70
  */
25
- readonly language: import("@angular/core").Signal<"af" | "sq" | "am" | "ar" | "hy" | "az" | "bn" | "bs" | "bg" | "ca" | "zh" | "zh-TW" | "hr" | "cs" | "da" | "fa-AF" | "nl" | "en" | "et" | "fa" | "tl" | "fi" | "fr" | "fr-CA" | "ka" | "de" | "el" | "gu" | "ht" | "ha" | "he" | "hi" | "hu" | "is" | "id" | "ga" | "it" | "ja" | "kn" | "kk" | "ko" | "lv" | "lt" | "mk" | "ms" | "ml" | "mt" | "mr" | "mn" | "no" | "ps" | "pl" | "pt" | "pt-PT" | "pa" | "ro" | "ru" | "sr" | "si" | "sk" | "sl" | "so" | "es" | "es-MX" | "sw" | "sv" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "uz" | "vi" | "cy">;
71
+ readonly language: import("@angular/core").Signal<"it" | "hr" | "th" | "tr" | "af" | "sq" | "am" | "ar" | "hy" | "az" | "bn" | "bs" | "bg" | "ca" | "zh" | "zh-TW" | "cs" | "da" | "fa-AF" | "nl" | "en" | "et" | "fa" | "tl" | "fi" | "fr" | "fr-CA" | "ka" | "de" | "el" | "gu" | "ht" | "ha" | "he" | "hi" | "hu" | "is" | "id" | "ga" | "ja" | "kn" | "kk" | "ko" | "lv" | "lt" | "mk" | "ms" | "ml" | "mt" | "mr" | "mn" | "no" | "ps" | "pl" | "pt" | "pt-PT" | "pa" | "ro" | "ru" | "sr" | "si" | "sk" | "sl" | "so" | "es" | "es-MX" | "sw" | "sv" | "ta" | "te" | "uk" | "ur" | "uz" | "vi" | "cy">;
26
72
  readonly dataLoader: import("@angular/core").Signal<DataLoader<DataLoaderLoadParams, string, string>>;
27
73
  constructor();
28
74
  setLanguage(language: Languages): void;
29
- translate(text: string, params?: TranslateParams): Promise<string>;
75
+ translate(text: string, { v, ...params }?: TranslateParams): Promise<string>;
30
76
  /** @ignore */
31
- cacheKey(text: string, params?: TranslateParams): string;
77
+ cacheKey(text: string, params?: Omit<TranslateParams, 'v'>): string;
32
78
  static ɵfac: i0.ɵɵFactoryDeclaration<MagicTranslate, never>;
33
79
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
34
80
  }
35
81
  export declare class Translate {
36
- readonly lang: import("@angular/core").InputSignal<"af" | "sq" | "am" | "ar" | "hy" | "az" | "bn" | "bs" | "bg" | "ca" | "zh" | "zh-TW" | "hr" | "cs" | "da" | "fa-AF" | "nl" | "en" | "et" | "fa" | "tl" | "fi" | "fr" | "fr-CA" | "ka" | "de" | "el" | "gu" | "ht" | "ha" | "he" | "hi" | "hu" | "is" | "id" | "ga" | "it" | "ja" | "kn" | "kk" | "ko" | "lv" | "lt" | "mk" | "ms" | "ml" | "mt" | "mr" | "mn" | "no" | "ps" | "pl" | "pt" | "pt-PT" | "pa" | "ro" | "ru" | "sr" | "si" | "sk" | "sl" | "so" | "es" | "es-MX" | "sw" | "sv" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "uz" | "vi" | "cy" | undefined>;
82
+ readonly lang: import("@angular/core").InputSignal<"it" | "hr" | "th" | "tr" | "af" | "sq" | "am" | "ar" | "hy" | "az" | "bn" | "bs" | "bg" | "ca" | "zh" | "zh-TW" | "cs" | "da" | "fa-AF" | "nl" | "en" | "et" | "fa" | "tl" | "fi" | "fr" | "fr-CA" | "ka" | "de" | "el" | "gu" | "ht" | "ha" | "he" | "hi" | "hu" | "is" | "id" | "ga" | "ja" | "kn" | "kk" | "ko" | "lv" | "lt" | "mk" | "ms" | "ml" | "mt" | "mr" | "mn" | "no" | "ps" | "pl" | "pt" | "pt-PT" | "pa" | "ro" | "ru" | "sr" | "si" | "sk" | "sl" | "so" | "es" | "es-MX" | "sw" | "sv" | "ta" | "te" | "uk" | "ur" | "uz" | "vi" | "cy" | undefined>;
37
83
  readonly formality: import("@angular/core").InputSignal<"formal" | "informal" | undefined>;
84
+ /**
85
+ * Lets this use of the text be edited on its own in the webapp. Its edited
86
+ * translation stays when the text changes.
87
+ */
88
+ readonly id: import("@angular/core").InputSignal<string | undefined>;
89
+ /**
90
+ * Values that are not translated, referenced as `$name` in the content. The
91
+ * text is translated once as a template and cached, whatever the values are.
92
+ */
93
+ readonly v: import("@angular/core").InputSignal<Variables | undefined>;
38
94
  private readonly magicTranslate;
39
95
  private readonly sanitizer;
40
96
  private readonly sourceRef;
@@ -42,6 +98,7 @@ export declare class Translate {
42
98
  private readonly source;
43
99
  protected readonly translated: import("@angular/core").WritableSignal<boolean>;
44
100
  private lastRequestId;
101
+ private lastTemplate?;
45
102
  constructor();
46
103
  private render;
47
104
  static ɵfac: i0.ɵɵFactoryDeclaration<Translate, never>;
@@ -56,6 +113,16 @@ export declare class Translate {
56
113
  "required": false;
57
114
  "isSignal": true;
58
115
  };
116
+ "id": {
117
+ "alias": "id";
118
+ "required": false;
119
+ "isSignal": true;
120
+ };
121
+ "v": {
122
+ "alias": "v";
123
+ "required": false;
124
+ "isSignal": true;
125
+ };
59
126
  }, {}, never, ["*"], true, never>;
60
127
  }
61
128
  export declare const T: typeof Translate;
@@ -63,6 +130,9 @@ export declare const T: typeof Translate;
63
130
  * Translates a plain string, e.g. for attributes:
64
131
  * `<input [placeholder]="'Search' | t" />`. Returns the original text until
65
132
  * the translation has loaded.
133
+ *
134
+ * Values that should not be translated go in `v` and are referenced as `$name`:
135
+ * `'Search $count products' | t: { v: { count } }`.
66
136
  */
67
137
  export declare class TranslatePipe implements PipeTransform {
68
138
  private readonly magicTranslate;
@@ -70,8 +140,9 @@ export declare class TranslatePipe implements PipeTransform {
70
140
  private readonly isServer;
71
141
  private key?;
72
142
  private translation?;
73
- transform(text: string, params?: TranslateParams): string;
143
+ transform(text: string, { v, ...params }?: TranslateParams): string;
74
144
  static ɵfac: i0.ɵɵFactoryDeclaration<TranslatePipe, never>;
75
145
  static ɵpipe: i0.ɵɵPipeDeclaration<TranslatePipe, "t", true>;
76
146
  }
147
+ export type { Variables };
77
148
  //# sourceMappingURL=magic-translate-angular.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"magic-translate-angular.d.ts","names":[],"sources":["../../src/lib/magic-translate-config.ts","../../src/lib/provide-magic-translate.ts","../../src/lib/magic-translate.service.ts","../../src/lib/translate.component.ts","../../src/lib/translate.pipe.ts"],"mappings":";;;;qBAGa,wBAAsB,eAAA;;;;;wBCMnB,sBACd,QAAQ,uBACP;;;;;YCIS,eAAe;YAEf,kBAAkB,KAAK;qBAGtB;mBACM;mBACA;WAER;WACA;;;;WAKA,kCAAQ;WAIR,oCAAU,OAAA,WAAA;;EAwBnB,YAAY,UAAU;EAItB,UAAU,cAAc,SAAQ,kBAAuB;;EAKvD,SAAS,cAAc,SAAQ;SA/CpB,MAAA,GAAA,qBAAA;;;qBC8DA;WACF,8BAAI;WACJ,mCAAS;mBAED;mBACA;mBACA;mBAEA;mBAGA;qBACE,oCAAU;UACrB;;UA6CA;SA1DG,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;IAAS;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;;;qBA4ET,UAAC;;;;;;qBC9ID,yBAAyB;mBACnB;mBACA;mBACA;UAET;UACA;EAER,UAAU,cAAc,SAAQ;SARrB,MAAA,GAAA,qBAAA;SAAA,OAAA,GAAA,kBAAA"}
1
+ {"version":3,"file":"magic-translate-angular.d.ts","names":[],"sources":["../../src/lib/magic-translate-config.ts","../../src/lib/provide-magic-translate.ts","../../src/lib/variables.ts","../../src/lib/magic-translate.service.ts","../../src/lib/translate.component.ts","../../src/lib/translate.pipe.ts"],"mappings":";;;;qBAGa,wBAAsB,eAAA;;;;;wBCMnB,sBACd,QAAQ,uBACP;;;;KCLS,YAAY;;;;;;;;;;iBAgBR,UAAU,WAAW;;EAWnB,QAAA;;;UAiBD;EACf;EACA;EAEA;;iBAGc,gBAAgB;iBAQhB,WAAW,cAAc,WAAW,YAAY;;;;;iBAShD,iBAAiB,cAAc;;;;;iBAmB/B,eAAe,cAAc,WAAW,YAAY;;;;;iBAcpD,YAAY,SAAS,SAAS;;;;;YCtFlC,eAAe;iBAEV,wBAAwB,KAAK;;;;;EAK5C,IAAI;;qBAIO;mBACM;mBACA;WAER;WACA;;;;WAKA,kCAAQ;WAIR,oCAAU,OAAA,WAAA;;EA0BnB,YAAY,UAAU;EAItB,UACE,gBACE,MAAM,WAAU,kBACjB;;EAYH,SAAS,cAAc,SAAQ,KAAK;SA3DzB,MAAA,GAAA,qBAAA;;;qBC8DA;WACF,8BAAI;WACJ,mCAAS;;;;;WAKT,4BAAE;;;;;WAKF,2BAAC,YAAA;mBAEO;mBACA;mBACA;mBAEA;mBAGA;qBACE,oCAAU;UACrB;UACA;;UA0DA;SAlFG,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;IAAS;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;;;qBA2GT,UAAC;;;;;;;;;qBCrLD,yBAAyB;mBACnB;mBACA;mBACA;UAET;UACA;EAER,UAAU,gBAAgB,MAAM,WAAU;SAR/B,MAAA,GAAA,qBAAA;SAAA,OAAA,GAAA,kBAAA"}