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 + +``` + +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 + +``` + ## 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 {