diff --git a/README.md b/README.md
index 33e04a7..7fc8119 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 translation params to all child elements.
+
+```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 0000000..bfaf9fe
--- /dev/null
+++ b/projects/ngx-translate/core/src/lib/translate-context.directive.ts
@@ -0,0 +1,30 @@
+import { Directive, Inject, Input, Optional, Self, SkipSelf, 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,
+ @Inject(TranslateService) @Optional() @SkipSelf() private readonly parentContext: TranslateService,
+ private templateRef: TemplateRef,
+ private viewContainer: ViewContainerRef
+ ) {
+ this.contextTranslateService.parentContext = this.parentContext;
+ }
+
+
+ @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-context.service.ts b/projects/ngx-translate/core/src/lib/translate-context.service.ts
new file mode 100644
index 0000000..d2b21c1
--- /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
new file mode 100644
index 0000000..03a888d
--- /dev/null
+++ b/projects/ngx-translate/core/src/lib/translate-namespace.directive.ts
@@ -0,0 +1,27 @@
+import { Directive, Inject, Input, Optional, Self, SkipSelf, 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,
+ @Inject(TranslateService) @Optional() @SkipSelf() private readonly parentContext: TranslateService,
+ private templateRef: TemplateRef,
+ private viewContainer: ViewContainerRef
+ ) {
+ this.contextTranslateService.parentContext = this.parentContext;
+ }
+
+
+ @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 ed7d484..9145481 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";
@@ -16,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;
@@ -33,11 +37,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 0000000..fe91956
--- /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 0000000..1e5645a
--- /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');
+ });
+
+});