Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add *translateContext & *translateNamespace directives #1358

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -357,6 +360,85 @@ To render them, simply use the `innerHTML` attribute with the pipe on any elemen
<div [innerHTML]="'HELLO' | translate"></div>
```

#### 7. Use translate namespace:

You can namespace your keys using the `*translateNamespace` structural directive

```html
<ul *translateNamespace="'somApp.pageX.contentComponent'">
<li translate>key1</li>
<li translate>key2</li>
<li translate>key3</li>
</ul>
```

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
<div *translateNamespace="'someApp'">
<router-outlet></router-outlet>
</div>
```

```html
// pageX.component.html
<div *translateNamespace="'pageX'">
<content-component></content-component>
</div>
```

```html
// content.component.html
<ul *translateNamespace="'contentComponent'">
<li translate>key1</li>
<li translate>key2</li>
<li translate>key3</li>
</ul>
```

> **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
<ul *translateContext="person">
<li translate>person.bio.nameColumn</li>
<li translate>person.bio.addressColumn</li>
<li translate>person.bio.age</li>
</ul>
```

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
<ul *translateContext="person; namespace: 'person.bio' ">
<li translate>nameColumn</li>
<li translate>addressColumn</li>
<li translate>age</li>
</ul>
```

## API

### TranslateService
Expand Down
30 changes: 30 additions & 0 deletions projects/ngx-translate/core/src/lib/translate-context.directive.ts
Original file line number Diff line number Diff line change
@@ -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<any>,
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;
}
}
31 changes: 31 additions & 0 deletions projects/ngx-translate/core/src/lib/translate-context.service.ts
Original file line number Diff line number Diff line change
@@ -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<string>, interpolateParams?: any | undefined): Observable<any> {

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$;
}

}
Original file line number Diff line number Diff line change
@@ -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<any>,
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);
}
}
12 changes: 10 additions & 2 deletions projects/ngx-translate/core/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -33,11 +37,15 @@ export interface TranslateModuleConfig {
@NgModule({
declarations: [
TranslatePipe,
TranslateDirective
TranslateDirective,
TranslateContextDirective,
TranslateNamespaceDirective
],
exports: [
TranslatePipe,
TranslateDirective
TranslateDirective,
TranslateContextDirective,
TranslateNamespaceDirective
]
})
export class TranslateModule {
Expand Down
127 changes: 127 additions & 0 deletions projects/ngx-translate/core/tests/translate-context.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -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: `
<div *translateContext="rootContext; namespace 'overrides'">
<div class="root-with-context" translate="LABEL"></div>
<child-component></child-component>
</div>

`
})
class RootComponent {
rootContext = { param1: 'r1', param2: 'r2', param3: 'r3' };
}

@Component({
selector: 'child-component',
template: `
<div class="child-no-context" translate="LABEL"></div>

<div *translateContext="childContext">
<div class="child-with-context" translate="LABEL"></div>
<grandchild-component></grandchild-component>
</div>
`
})
class ChildComponent {
childContext = { param2: 'c2' }
}

@Component({
selector: 'grandchild-component',
template: `
<div class="grandchild-no-context" translate="LABEL"></div>

<div *translateContext="grandchildContext">
<div class="grandchild-with-context" translate="LABEL"></div>
</div>


<div class="grandchild-key1" translate="gc.key1"></div>

<div *translateContext="{}; namespace 'gc'">
<div class="grandchild-key2" translate="key2"></div>
<div class="grandchild-key3" translate="key3"></div>
</div>
`
})
class GrandChildComponent {
grandchildContext = { param3: 'gc3' }
}


describe('TranslateContextDirective', () => {
let fixture: ComponentFixture<RootComponent>;
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');
});

});
Loading