Skip to content

Commit

Permalink
feat(TranslateDirective): the directive if finally here!
Browse files Browse the repository at this point in the history
Closes #31
  • Loading branch information
ocombe authored Dec 5, 2016
1 parent 4ef3d0c commit 98a2350
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 76 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,11 @@ The `TranslateParser` understands nested JSON objects. This means that you can h

You can then access the value by using the dot notation, in this case `HOME.HELLO`.

#### 4. Use the service or the pipe:
#### 4. Use the service, the pipe or the directive:

You can either use the `TranslateService` or the `TranslatePipe` to get your translation values.
You can either use the `TranslateService`, the `TranslatePipe` or the `TranslateDirective` to get your translation values.

With the service, it looks like this.
With the **service**, it looks like this:

```ts
translate.get('HELLO', {value: 'world'}).subscribe((res: string) => {
Expand All @@ -180,7 +180,7 @@ translate.get('HELLO', {value: 'world'}).subscribe((res: string) => {
});
```

And this is how you do it with the pipe.
This is how you do it with the **pipe**:

```html
<div>{{ 'HELLO' | translate:param }}</div>
Expand All @@ -191,6 +191,16 @@ And in your component define `param` like this:
param = {value: 'world'};
```

This is how you use the **directive**:
```html
<div [translate]="'HELLO'" [translateparams]="{param: 'world'}"></div>
```

Or even simpler using the content of your element as a key:
```html
<div translate [translateparams]="{param: 'world'}">HELLO</div>
```

#### 5. Use HTML tags:

You can easily use raw HTML tags within your translations.
Expand All @@ -201,7 +211,7 @@ You can easily use raw HTML tags within your translations.
}
```

To render them, simply use the `innerHTML` attributeon any element.
To render them, simply use the `innerHTML` attribute with the pipe on any element.

```html
<div [innerHTML]="'HELLO' | translate"></div>
Expand Down
6 changes: 5 additions & 1 deletion config/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = function(config) {
frameworks: ['jasmine'],

// list of files to exclude
exclude: [ ],
exclude: [],

/*
* list of files / patterns to load in the browser
Expand Down Expand Up @@ -36,6 +36,10 @@ module.exports = function(config) {

reporters: [ 'mocha', 'coverage', 'remap-coverage' ],

mochaReporter: {
ignoreSkipped: true
},

// web server port
port: 9876,

Expand Down
8 changes: 6 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {NgModule, ModuleWithProviders} from "@angular/core";
import {Http, HttpModule} from "@angular/http";
import {TranslatePipe} from "./src/translate.pipe";
import {TranslateService, TranslateLoader, TranslateStaticLoader} from "./src/translate.service";
import {TranslateDirective} from "./src/translate.directive";

export * from "./src/translate.pipe";
export * from "./src/translate.service";
export * from "./src/translate.parser";
export * from "./src/translate.directive";

export function translateLoaderFactory(http: Http) {
return new TranslateStaticLoader(http);
Expand All @@ -14,11 +16,13 @@ export function translateLoaderFactory(http: Http) {
@NgModule({
imports: [HttpModule],
declarations: [
TranslatePipe
TranslatePipe,
TranslateDirective
],
exports: [
HttpModule, // todo remove this when removing the loader from core
TranslatePipe
TranslatePipe,
TranslateDirective
]
})
export class TranslateModule {
Expand Down
2 changes: 1 addition & 1 deletion ng2-translate.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './index';
export * from './index';
90 changes: 90 additions & 0 deletions src/translate.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {Directive, ElementRef, AfterViewChecked, Input, OnDestroy} from "@angular/core";
import {Subscription} from "rxjs";
import {isDefined} from "./util";
import {TranslateService, LangChangeEvent} from "./translate.service";

@Directive({
selector: '[translate]'
})
export class TranslateDirective implements AfterViewChecked, OnDestroy {
key: string;
lastParams: any;
onLangChangeSub: Subscription;

@Input() set translate(key: string) {
if(key) {
this.key = key;
this.checkNodes();
}
}

@Input() translateParams: any;

constructor(private translateService: TranslateService, private element: ElementRef) {
// subscribe to onLangChange event, in case the language changes
if(!this.onLangChangeSub) {
this.onLangChangeSub = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.checkNodes(true);
});
}
}

ngAfterViewChecked() {
this.checkNodes();
}

checkNodes(langChanged = false) {
let nodes: NodeList = this.element.nativeElement.childNodes;
for(let i = 0; i < nodes.length; ++i) {
let node: any = nodes[i];
if(node.nodeType === 3) { // node type 3 is a text node
let key: string;
if(this.key) {
key = this.key;
} else {
let content = node.textContent.trim();
if(content.length) {
// we want to use the content as a key, not the translation value
if(content !== node.currentValue) {
key = content;
// the content was changed from the user, we'll use it as a reference if needed
node.originalContent = node.textContent;
} else if(node.originalContent && langChanged) { // the content seems ok, but the lang has changed
// the current content is the translation, not the key, use the last real content as key
key = node.originalContent.trim();
}
}
}
this.updateValue(key, node);
}
}
}

updateValue(key: string, node: any) {
if(key) {
let interpolateParams: Object = this.translateParams;
if(node.lastKey === key && this.lastParams === interpolateParams) {
return;
}

this.lastParams = interpolateParams;
this.translateService.get(key, interpolateParams).subscribe((res: string | any) => {
if(res !== key) {
node.lastKey = key;
}
if(!node.originalContent) {
node.originalContent = node.textContent;
}
node.currentValue = isDefined(res) ? res : (node.originalContent || key);
// we replace in the original content to preserve spaces that we might have trimmed
node.textContent = this.key ? node.currentValue : node.originalContent.replace(key, node.currentValue);
});
}
}

ngOnDestroy() {
if(this.onLangChangeSub) {
this.onLangChangeSub.unsubscribe();
}
}
}
16 changes: 9 additions & 7 deletions src/translate.parser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {isDefined} from "./util";

export class Parser {
templateMatcher: RegExp = /{{\s?([^{}\s]*)\s?}}/g;

Expand All @@ -9,13 +11,13 @@ export class Parser {
* @returns {string}
*/
public interpolate(expr: string, params?: any): string {
if (typeof expr !== 'string' || !params) {
if(typeof expr !== 'string' || !params) {
return expr;
}

return expr.replace(this.templateMatcher, (substring: string, b: string) => {
let r = this.getValue(params, b);
return typeof r !== 'undefined' ? r : substring;
return isDefined(r) ? r : substring;
});
}

Expand All @@ -31,16 +33,16 @@ export class Parser {
key = '';
do {
key += keys.shift();
if (target!==undefined && target[key] !== undefined && (typeof target[key] === 'object' || !keys.length)) {
if(isDefined(target) && isDefined(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
target = target[key];
key = '';
} else if (!keys.length) {
} else if(!keys.length) {
target = undefined;
} else {
key += '.';
}
} while (keys.length);
} while(keys.length);

return target;
}

Expand Down
60 changes: 4 additions & 56 deletions src/translate.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {PipeTransform, Pipe, Injectable, EventEmitter, OnDestroy, ChangeDetectorRef} from '@angular/core';
import {TranslateService, LangChangeEvent, TranslationChangeEvent} from './translate.service';
import {equals, isDefined} from "./util";

@Injectable()
@Pipe({
Expand All @@ -16,60 +17,6 @@ export class TranslatePipe implements PipeTransform, OnDestroy {
constructor(private translate: TranslateService, private _ref: ChangeDetectorRef) {
}

/* tslint:disable */
/**
* @name equals
*
* @description
* Determines if two objects or two values are equivalent.
*
* Two objects or values are considered equivalent if at least one of the following is true:
*
* * Both objects or values pass `===` comparison.
* * Both objects or values are of the same type and all of their properties are equal by
* comparing them with `equals`.
*
* @param {*} o1 Object or value to compare.
* @param {*} o2 Object or value to compare.
* @returns {boolean} True if arguments are equal.
*/
private equals(o1: any, o2: any): boolean {
if(o1 === o2) return true;
if(o1 === null || o2 === null) return false;
if(o1 !== o1 && o2 !== o2) return true; // NaN === NaN
let t1 = typeof o1, t2 = typeof o2, length: number, key: any, keySet: any;
if(t1 == t2 && t1 == 'object') {
if(Array.isArray(o1)) {
if(!Array.isArray(o2)) return false;
if((length = o1.length) == o2.length) {
for(key = 0; key < length; key++) {
if(!this.equals(o1[key], o2[key])) return false;
}
return true;
}
} else {
if(Array.isArray(o2)) {
return false;
}
keySet = Object.create(null);
for(key in o1) {
if(!this.equals(o1[key], o2[key])) {
return false;
}
keySet[key] = true;
}
for(key in o2) {
if(!(key in keySet) && typeof o2[key] !== 'undefined') {
return false;
}
}
return true;
}
}
return false;
}
/* tslint:enable */

updateValue(key: string, interpolateParams?: Object, translations?: any): void {
let onTranslation = (res: string) => {
this.value = res !== undefined ? res : key;
Expand All @@ -91,13 +38,14 @@ export class TranslatePipe implements PipeTransform, OnDestroy {
if(!query || query.length === 0) {
return query;
}

// if we ask another time for the same key, return the last value
if(this.equals(query, this.lastKey) && this.equals(args, this.lastParams)) {
if(equals(query, this.lastKey) && equals(args, this.lastParams)) {
return this.value;
}

let interpolateParams: Object;
if(args.length && args[0] !== null) {
if(isDefined(args[0]) && args.length) {
if(typeof args[0] === 'string' && args[0].length) {
// we accept objects written in the template such as {n:1}, {'n':1}, {n:'v'}
// which is why we might need to change it to real JSON objects such as {"n":1} or {"n":"v"}
Expand Down
7 changes: 4 additions & 3 deletions src/translate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "rxjs/add/operator/merge";
import "rxjs/add/operator/toArray";

import {Parser} from "./translate.parser";
import {isDefined} from "./util";

export interface TranslationChangeEvent {
translations: any;
Expand Down Expand Up @@ -46,7 +47,7 @@ export interface MissingTranslationHandlerParams {
declare interface Window {
navigator: any;
}
declare var window: Window;
declare const window: Window;

export abstract class MissingTranslationHandler {
/**
Expand Down Expand Up @@ -294,7 +295,7 @@ export class TranslateService {
* @returns {any} the translated key, or an object of translated keys
*/
public get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> {
if(!key) {
if(!isDefined(key) || !key.length) {
throw new Error(`Parameter "key" required`);
}
// check if we are loading a new translation to use
Expand Down Expand Up @@ -334,7 +335,7 @@ export class TranslateService {
* @returns {string}
*/
public instant(key: string|Array<string>, interpolateParams?: Object): string|any {
if(!key) {
if(!isDefined(key) || !key.length) {
throw new Error(`Parameter "key" required`);
}

Expand Down
Loading

0 comments on commit 98a2350

Please sign in to comment.