From dd971b85c6a950b839c321a6821b79a196c76eb9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Fortin?=
<17817939+FortinFred@users.noreply.github.com>
Date: Mon, 10 Jan 2022 14:01:07 -0500
Subject: [PATCH 1/3] feat(core): add *translateContext & *translateNamespace
directives
---
README.md | 82 +++++++++++
.../src/lib/translate-context.directive.ts | 27 ++++
.../src/lib/translate-namespace.directive.ts | 24 ++++
projects/ngx-translate/core/src/public_api.ts | 10 +-
.../tests/translate-context.directive.spec.ts | 127 ++++++++++++++++++
.../translate-namespace.directive.spec.ts | 91 +++++++++++++
6 files changed, 359 insertions(+), 2 deletions(-)
create mode 100644 projects/ngx-translate/core/src/lib/translate-context.directive.ts
create mode 100644 projects/ngx-translate/core/src/lib/translate-namespace.directive.ts
create mode 100644 projects/ngx-translate/core/tests/translate-context.directive.spec.ts
create mode 100644 projects/ngx-translate/core/tests/translate-namespace.directive.spec.ts
diff --git a/README.md b/README.md
index 33e04a71..2e0c033b 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,9 @@ Get the complete changelog here: https://github.com/ngx-translate/core/releases
* [Define the translations](#4-define-the-translations)
* [Use the service, the pipe or the directive](#5-use-the-service-the-pipe-or-the-directive)
* [Use HTML tags](#6-use-html-tags)
+ * [Use translateNamespace directive](#7-use-translate-namespace)
+ * [Use translateContext directive](#8-use-translate-context)
+
* [API](#api)
* [TranslateService](#translateservice)
* [Properties](#properties)
@@ -357,6 +360,85 @@ To render them, simply use the `innerHTML` attribute with the pipe on any elemen
```
+#### 7. Use translate namespace:
+
+You can namespace your keys using the `*translateNamespace` structural directive
+
+```html
+
+```
+
+Would translate the `somApp.pageX.contentComponent.key1`, `somApp.pageX.contentComponent.key2` and `somApp.pageX.contentComponent.key3` keys.
+
+The namespaces directive are also stackable:
+
+```html
+// app.component.html
+
+
+
+```
+
+```html
+// pageX.component.html
+
+
+
+```
+
+```html
+// content.component.html
+
+```
+
+> **Note:**
+>
+> To avoid breaking changes, the translation resolution will fallback to a non namespaced key if the key isn't found in a given namespace.
+
+#### 8. Use translate context:
+
+You can use the `*translateContext` structural directive to provide the context of all child translation to avoir to repeat the translateParams directive.
+
+```json
+{
+ "person": {
+ "bio": {
+ "nameColumn": "{{firstName}} {{lastName}}",
+ "addressColumn": "{{address}} {{city}} {{state}}",
+ "age": "{{age}} years old"
+ }
+ }
+}
+```
+
+```html
+
+ - person.bio.nameColumn
+ - person.bio.addressColumn
+ - person.bio.age
+
+```
+
+The context are also stackable. Meaning that context from parent dom element will be inherited and merged with the current context.
+
+Since `*translateNamespace` and `*translateContext` are structural directives and Angular limits one structural directive per element, it is possible to provide the namespace on the `*translateContext` directive:
+
+```html
+
+ - nameColumn
+ - addressColumn
+ - age
+
+```
+
## API
### TranslateService
diff --git a/projects/ngx-translate/core/src/lib/translate-context.directive.ts b/projects/ngx-translate/core/src/lib/translate-context.directive.ts
new file mode 100644
index 00000000..23a7bcd4
--- /dev/null
+++ b/projects/ngx-translate/core/src/lib/translate-context.directive.ts
@@ -0,0 +1,27 @@
+import { Directive, Inject, Input, Self, TemplateRef, ViewContainerRef } from '@angular/core';
+import { TranslateContextService } from './translate-context.service';
+import { TranslateService } from './translate.service';
+
+@Directive({
+ selector: '[translateContext],[ngx-translateContext]',
+ providers: [{ provide: TranslateService, useClass: TranslateContextService }]
+})
+export class TranslateContextDirective {
+
+ constructor(
+ @Inject(TranslateService) @Self() private contextTranslateService: TranslateContextService,
+ private templateRef: TemplateRef,
+ private viewContainer: ViewContainerRef
+ ) { }
+
+
+ @Input() set translateContext(params: {}) {
+ this.contextTranslateService.params = params;
+ this.viewContainer.clear();
+ this.viewContainer.createEmbeddedView(this.templateRef);
+ }
+
+ @Input() set translateContextNamespace(namespace: string) {
+ this.contextTranslateService.namespace = namespace;
+ }
+}
\ No newline at end of file
diff --git a/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts b/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts
new file mode 100644
index 00000000..fc71790c
--- /dev/null
+++ b/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts
@@ -0,0 +1,24 @@
+import { Directive, Inject, Input, Self, TemplateRef, ViewContainerRef } from '@angular/core';
+import { TranslateContextService } from './translate-context.service';
+import { TranslateService } from './translate.service';
+
+
+@Directive({
+ selector: '[translateNamespace],[ngx-translateNamespace]',
+ providers: [{ provide: TranslateService, useClass: TranslateContextService }]
+})
+export class TranslateNamespaceDirective {
+
+ constructor(
+ @Inject(TranslateService) @Self() private contextTranslateService: TranslateContextService,
+ private templateRef: TemplateRef,
+ private viewContainer: ViewContainerRef
+ ) { }
+
+
+ @Input() set translateNamespace(namespace: string) {
+ this.contextTranslateService.namespace = namespace;
+ this.viewContainer.clear();
+ this.viewContainer.createEmbeddedView(this.templateRef);
+ }
+}
\ No newline at end of file
diff --git a/projects/ngx-translate/core/src/public_api.ts b/projects/ngx-translate/core/src/public_api.ts
index ed7d484b..c73eb264 100644
--- a/projects/ngx-translate/core/src/public_api.ts
+++ b/projects/ngx-translate/core/src/public_api.ts
@@ -7,6 +7,8 @@ import {TranslateDirective} from "./lib/translate.directive";
import {TranslatePipe} from "./lib/translate.pipe";
import {TranslateStore} from "./lib/translate.store";
import {USE_DEFAULT_LANG, DEFAULT_LANGUAGE, USE_STORE, TranslateService, USE_EXTEND} from "./lib/translate.service";
+import { TranslateContextDirective } from "./lib/translate-context.directive";
+import { TranslateNamespaceDirective } from "./lib/translate-namespace.directive";
export * from "./lib/translate.loader";
export * from "./lib/translate.service";
@@ -33,11 +35,15 @@ export interface TranslateModuleConfig {
@NgModule({
declarations: [
TranslatePipe,
- TranslateDirective
+ TranslateDirective,
+ TranslateContextDirective,
+ TranslateNamespaceDirective
],
exports: [
TranslatePipe,
- TranslateDirective
+ TranslateDirective,
+ TranslateContextDirective,
+ TranslateNamespaceDirective
]
})
export class TranslateModule {
diff --git a/projects/ngx-translate/core/tests/translate-context.directive.spec.ts b/projects/ngx-translate/core/tests/translate-context.directive.spec.ts
new file mode 100644
index 00000000..fe919564
--- /dev/null
+++ b/projects/ngx-translate/core/tests/translate-context.directive.spec.ts
@@ -0,0 +1,127 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateContextDirective } from '../src/lib/translate-context.directive';
+import { TranslateModule, TranslateService } from '../src/public_api';
+
+
+const labels = {
+ LABEL: 'context: {{param1}} {{param2}} {{param3}}',
+
+ gc: {
+ key1: 'gc.k1',
+ key2: 'gc.k2',
+ key3: 'gc.k3'
+ },
+
+ overrides: {
+ gc: {
+ key1: 'root.k1',
+ key2: 'root.k2'
+ }
+ }
+
+};
+
+@Component({
+ selector: 'root-component',
+ template: `
+
+
+ `
+})
+class RootComponent {
+ rootContext = { param1: 'r1', param2: 'r2', param3: 'r3' };
+}
+
+@Component({
+ selector: 'child-component',
+ template: `
+
+
+
+ `
+})
+class ChildComponent {
+ childContext = { param2: 'c2' }
+}
+
+@Component({
+ selector: 'grandchild-component',
+ template: `
+
+
+
+
+
+
+
+
+ `
+})
+class GrandChildComponent {
+ grandchildContext = { param3: 'gc3' }
+}
+
+
+describe('TranslateContextDirective', () => {
+ let fixture: ComponentFixture;
+ let nativeElement: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RootComponent, ChildComponent, GrandChildComponent, TranslateContextDirective],
+ imports: [TranslateModule.forRoot()]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ const translateService = TestBed.inject(TranslateService);
+ translateService.setTranslation('en', labels);
+ translateService.currentLang = 'en';
+
+ fixture = TestBed.createComponent(RootComponent);
+ fixture.detectChanges();
+ nativeElement = fixture.nativeElement;
+ });
+
+
+
+ it('should provide context params for immediate view children', () => {
+ expect(nativeElement.getElementsByClassName('root-with-context')[0].innerHTML).toBe('context: r1 r2 r3');
+ });
+
+ it('should inherit context params from parent component', () => {
+ expect(nativeElement.getElementsByClassName('child-no-context')[0].innerHTML).toBe('context: r1 r2 r3');
+ });
+
+ it('should merge context params over inherited context', () => {
+ expect(nativeElement.getElementsByClassName('child-with-context')[0].innerHTML).toBe('context: r1 c2 r3');
+ });
+
+ it('should inherit context params from parent and grand parent components', () => {
+ expect(nativeElement.getElementsByClassName('grandchild-no-context')[0].innerHTML).toBe('context: r1 c2 r3');
+ });
+
+ it('should merge context params over parent and grand parent context', () => {
+ expect(nativeElement.getElementsByClassName('grandchild-with-context')[0].innerHTML).toBe('context: r1 c2 gc3');
+ });
+
+ it('should prefix the key with the context namespace', () => {
+ expect(nativeElement.getElementsByClassName('grandchild-key1')[0].innerHTML).toBe('root.k1');
+ expect(nativeElement.getElementsByClassName('grandchild-key2')[0].innerHTML).toBe('root.k2');
+ expect(nativeElement.getElementsByClassName('grandchild-key3')[0].innerHTML).toBe('gc.k3');
+ });
+
+});
diff --git a/projects/ngx-translate/core/tests/translate-namespace.directive.spec.ts b/projects/ngx-translate/core/tests/translate-namespace.directive.spec.ts
new file mode 100644
index 00000000..1e5645aa
--- /dev/null
+++ b/projects/ngx-translate/core/tests/translate-namespace.directive.spec.ts
@@ -0,0 +1,91 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateNamespaceDirective } from '../src/lib/translate-namespace.directive';
+import { TranslateModule, TranslateService } from '../src/public_api';
+
+
+const labels = {
+ gc: {
+ key1: 'gc.k1',
+ key2: 'gc.k2',
+ key3: 'gc.k3'
+ },
+
+ overrides: {
+ gc: {
+ key1: 'root.k1',
+ key2: 'root.k2'
+ }
+ }
+
+};
+
+@Component({
+ selector: 'root-component',
+ template: `
+
+
+
+
+ `
+})
+class RootComponent {
+ rootContext = { param1: 'r1', param2: 'r2', param3: 'r3' };
+}
+
+@Component({
+ selector: 'child-component',
+ template: `
+
+ `
+})
+class ChildComponent {
+ childContext = { param2: 'c2' }
+}
+
+@Component({
+ selector: 'grandchild-component',
+ template: `
+
+
+
+ `
+})
+class GrandChildComponent {
+ grandchildContext = { param3: 'gc3' }
+}
+
+
+describe('TranslateNamespaceDirective', () => {
+ let fixture: ComponentFixture;
+ let nativeElement: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RootComponent, ChildComponent, GrandChildComponent, TranslateNamespaceDirective],
+ imports: [TranslateModule.forRoot()]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ const translateService = TestBed.inject(TranslateService);
+ translateService.setTranslation('en', labels);
+ translateService.currentLang = 'en';
+
+ fixture = TestBed.createComponent(RootComponent);
+ fixture.detectChanges();
+ nativeElement = fixture.nativeElement;
+ });
+
+
+ it('should prefix the key with the context namespace', () => {
+ expect(nativeElement.getElementsByClassName('grandchild-key1')[0].innerHTML).toBe('root.k1');
+ expect(nativeElement.getElementsByClassName('grandchild-key2')[0].innerHTML).toBe('root.k2');
+ expect(nativeElement.getElementsByClassName('grandchild-key3')[0].innerHTML).toBe('gc.k3');
+ });
+
+});
From ab3310a3b1e91de12da92297631d9dddc38d20ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Fortin?=
<17817939+FortinFred@users.noreply.github.com>
Date: Sun, 30 Oct 2022 20:16:00 -0400
Subject: [PATCH 2/3] fix forgotten translate-context.service
---
.../src/lib/translate-context.directive.ts | 7 +++--
.../core/src/lib/translate-context.service.ts | 31 +++++++++++++++++++
.../src/lib/translate-namespace.directive.ts | 7 +++--
projects/ngx-translate/core/src/public_api.ts | 2 ++
4 files changed, 43 insertions(+), 4 deletions(-)
create mode 100644 projects/ngx-translate/core/src/lib/translate-context.service.ts
diff --git a/projects/ngx-translate/core/src/lib/translate-context.directive.ts b/projects/ngx-translate/core/src/lib/translate-context.directive.ts
index 23a7bcd4..bfaf9fe9 100644
--- a/projects/ngx-translate/core/src/lib/translate-context.directive.ts
+++ b/projects/ngx-translate/core/src/lib/translate-context.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, Inject, Input, Self, TemplateRef, ViewContainerRef } from '@angular/core';
+import { Directive, Inject, Input, Optional, Self, SkipSelf, TemplateRef, ViewContainerRef } from '@angular/core';
import { TranslateContextService } from './translate-context.service';
import { TranslateService } from './translate.service';
@@ -10,9 +10,12 @@ export class TranslateContextDirective {
constructor(
@Inject(TranslateService) @Self() private contextTranslateService: TranslateContextService,
+ @Inject(TranslateService) @Optional() @SkipSelf() private readonly parentContext: TranslateService,
private templateRef: TemplateRef,
private viewContainer: ViewContainerRef
- ) { }
+ ) {
+ this.contextTranslateService.parentContext = this.parentContext;
+ }
@Input() set translateContext(params: {}) {
diff --git a/projects/ngx-translate/core/src/lib/translate-context.service.ts b/projects/ngx-translate/core/src/lib/translate-context.service.ts
new file mode 100644
index 00000000..d2b21c12
--- /dev/null
+++ b/projects/ngx-translate/core/src/lib/translate-context.service.ts
@@ -0,0 +1,31 @@
+import { Injectable } from "@angular/core";
+import { TranslateService } from "./translate.service";
+import { Observable, zip } from "rxjs";
+import { map } from "rxjs/operators";
+
+@Injectable()
+export class TranslateContextService extends TranslateService {
+
+ params = {};
+ namespace: string | null = null;
+
+ parentContext!: TranslateContextService | TranslateService;
+
+ public override get(key: string | Array, interpolateParams?: any | undefined): Observable {
+
+ const paramsWithContext = {...this.params, ...interpolateParams};
+
+ let result$ = this.parentContext.get(key, paramsWithContext);
+
+ if(this.namespace) {
+ const namespacedKey = `${this.namespace}.${key}`;
+
+ result$ = zip(result$, this.parentContext.get(namespacedKey, paramsWithContext)).pipe(
+ map( ([label, namespacedLabel]) => namespacedLabel === namespacedKey ? label : namespacedLabel )
+ );
+ }
+
+ return result$;
+ }
+
+}
diff --git a/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts b/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts
index fc71790c..03a888de 100644
--- a/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts
+++ b/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, Inject, Input, Self, TemplateRef, ViewContainerRef } from '@angular/core';
+import { Directive, Inject, Input, Optional, Self, SkipSelf, TemplateRef, ViewContainerRef } from '@angular/core';
import { TranslateContextService } from './translate-context.service';
import { TranslateService } from './translate.service';
@@ -11,9 +11,12 @@ export class TranslateNamespaceDirective {
constructor(
@Inject(TranslateService) @Self() private contextTranslateService: TranslateContextService,
+ @Inject(TranslateService) @Optional() @SkipSelf() private readonly parentContext: TranslateService,
private templateRef: TemplateRef,
private viewContainer: ViewContainerRef
- ) { }
+ ) {
+ this.contextTranslateService.parentContext = this.parentContext;
+ }
@Input() set translateNamespace(namespace: string) {
diff --git a/projects/ngx-translate/core/src/public_api.ts b/projects/ngx-translate/core/src/public_api.ts
index c73eb264..91454811 100644
--- a/projects/ngx-translate/core/src/public_api.ts
+++ b/projects/ngx-translate/core/src/public_api.ts
@@ -18,6 +18,8 @@ export * from "./lib/translate.compiler";
export * from "./lib/translate.directive";
export * from "./lib/translate.pipe";
export * from "./lib/translate.store";
+export * from "./lib/translate-context.directive";
+export * from "./lib/translate-namespace.directive";
export interface TranslateModuleConfig {
loader?: Provider;
From bf6ba64301483ee98c48c138824e3699975fb050 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Fortin?=
<17817939+FortinFred@users.noreply.github.com>
Date: Sun, 30 Oct 2022 20:24:54 -0400
Subject: [PATCH 3/3] doc: rephrase translateContext description
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 2e0c033b..7fc8119f 100644
--- a/README.md
+++ b/README.md
@@ -405,7 +405,7 @@ The namespaces directive are also stackable:
#### 8. Use translate context:
-You can use the `*translateContext` structural directive to provide the context of all child translation to avoir to repeat the translateParams directive.
+You can use the `*translateContext` structural directive to provide translation params to all child elements.
```json
{