Skip to content

Commit 98a2350

Browse files
authored
feat(TranslateDirective): the directive if finally here!
Closes #31
1 parent 4ef3d0c commit 98a2350

12 files changed

+308
-76
lines changed

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,11 @@ The `TranslateParser` understands nested JSON objects. This means that you can h
167167

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

170-
#### 4. Use the service or the pipe:
170+
#### 4. Use the service, the pipe or the directive:
171171

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

174-
With the service, it looks like this.
174+
With the **service**, it looks like this:
175175

176176
```ts
177177
translate.get('HELLO', {value: 'world'}).subscribe((res: string) => {
@@ -180,7 +180,7 @@ translate.get('HELLO', {value: 'world'}).subscribe((res: string) => {
180180
});
181181
```
182182

183-
And this is how you do it with the pipe.
183+
This is how you do it with the **pipe**:
184184

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

194+
This is how you use the **directive**:
195+
```html
196+
<div [translate]="'HELLO'" [translateparams]="{param: 'world'}"></div>
197+
```
198+
199+
Or even simpler using the content of your element as a key:
200+
```html
201+
<div translate [translateparams]="{param: 'world'}">HELLO</div>
202+
```
203+
194204
#### 5. Use HTML tags:
195205

196206
You can easily use raw HTML tags within your translations.
@@ -201,7 +211,7 @@ You can easily use raw HTML tags within your translations.
201211
}
202212
```
203213

204-
To render them, simply use the `innerHTML` attributeon any element.
214+
To render them, simply use the `innerHTML` attribute with the pipe on any element.
205215

206216
```html
207217
<div [innerHTML]="'HELLO' | translate"></div>

config/karma.conf.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module.exports = function(config) {
77
frameworks: ['jasmine'],
88

99
// list of files to exclude
10-
exclude: [ ],
10+
exclude: [],
1111

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

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

39+
mochaReporter: {
40+
ignoreSkipped: true
41+
},
42+
3943
// web server port
4044
port: 9876,
4145

index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import {NgModule, ModuleWithProviders} from "@angular/core";
22
import {Http, HttpModule} from "@angular/http";
33
import {TranslatePipe} from "./src/translate.pipe";
44
import {TranslateService, TranslateLoader, TranslateStaticLoader} from "./src/translate.service";
5+
import {TranslateDirective} from "./src/translate.directive";
56

67
export * from "./src/translate.pipe";
78
export * from "./src/translate.service";
89
export * from "./src/translate.parser";
10+
export * from "./src/translate.directive";
911

1012
export function translateLoaderFactory(http: Http) {
1113
return new TranslateStaticLoader(http);
@@ -14,11 +16,13 @@ export function translateLoaderFactory(http: Http) {
1416
@NgModule({
1517
imports: [HttpModule],
1618
declarations: [
17-
TranslatePipe
19+
TranslatePipe,
20+
TranslateDirective
1821
],
1922
exports: [
2023
HttpModule, // todo remove this when removing the loader from core
21-
TranslatePipe
24+
TranslatePipe,
25+
TranslateDirective
2226
]
2327
})
2428
export class TranslateModule {

ng2-translate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './index';
1+
export * from './index';

src/translate.directive.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {Directive, ElementRef, AfterViewChecked, Input, OnDestroy} from "@angular/core";
2+
import {Subscription} from "rxjs";
3+
import {isDefined} from "./util";
4+
import {TranslateService, LangChangeEvent} from "./translate.service";
5+
6+
@Directive({
7+
selector: '[translate]'
8+
})
9+
export class TranslateDirective implements AfterViewChecked, OnDestroy {
10+
key: string;
11+
lastParams: any;
12+
onLangChangeSub: Subscription;
13+
14+
@Input() set translate(key: string) {
15+
if(key) {
16+
this.key = key;
17+
this.checkNodes();
18+
}
19+
}
20+
21+
@Input() translateParams: any;
22+
23+
constructor(private translateService: TranslateService, private element: ElementRef) {
24+
// subscribe to onLangChange event, in case the language changes
25+
if(!this.onLangChangeSub) {
26+
this.onLangChangeSub = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
27+
this.checkNodes(true);
28+
});
29+
}
30+
}
31+
32+
ngAfterViewChecked() {
33+
this.checkNodes();
34+
}
35+
36+
checkNodes(langChanged = false) {
37+
let nodes: NodeList = this.element.nativeElement.childNodes;
38+
for(let i = 0; i < nodes.length; ++i) {
39+
let node: any = nodes[i];
40+
if(node.nodeType === 3) { // node type 3 is a text node
41+
let key: string;
42+
if(this.key) {
43+
key = this.key;
44+
} else {
45+
let content = node.textContent.trim();
46+
if(content.length) {
47+
// we want to use the content as a key, not the translation value
48+
if(content !== node.currentValue) {
49+
key = content;
50+
// the content was changed from the user, we'll use it as a reference if needed
51+
node.originalContent = node.textContent;
52+
} else if(node.originalContent && langChanged) { // the content seems ok, but the lang has changed
53+
// the current content is the translation, not the key, use the last real content as key
54+
key = node.originalContent.trim();
55+
}
56+
}
57+
}
58+
this.updateValue(key, node);
59+
}
60+
}
61+
}
62+
63+
updateValue(key: string, node: any) {
64+
if(key) {
65+
let interpolateParams: Object = this.translateParams;
66+
if(node.lastKey === key && this.lastParams === interpolateParams) {
67+
return;
68+
}
69+
70+
this.lastParams = interpolateParams;
71+
this.translateService.get(key, interpolateParams).subscribe((res: string | any) => {
72+
if(res !== key) {
73+
node.lastKey = key;
74+
}
75+
if(!node.originalContent) {
76+
node.originalContent = node.textContent;
77+
}
78+
node.currentValue = isDefined(res) ? res : (node.originalContent || key);
79+
// we replace in the original content to preserve spaces that we might have trimmed
80+
node.textContent = this.key ? node.currentValue : node.originalContent.replace(key, node.currentValue);
81+
});
82+
}
83+
}
84+
85+
ngOnDestroy() {
86+
if(this.onLangChangeSub) {
87+
this.onLangChangeSub.unsubscribe();
88+
}
89+
}
90+
}

src/translate.parser.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {isDefined} from "./util";
2+
13
export class Parser {
24
templateMatcher: RegExp = /{{\s?([^{}\s]*)\s?}}/g;
35

@@ -9,13 +11,13 @@ export class Parser {
911
* @returns {string}
1012
*/
1113
public interpolate(expr: string, params?: any): string {
12-
if (typeof expr !== 'string' || !params) {
14+
if(typeof expr !== 'string' || !params) {
1315
return expr;
1416
}
15-
17+
1618
return expr.replace(this.templateMatcher, (substring: string, b: string) => {
1719
let r = this.getValue(params, b);
18-
return typeof r !== 'undefined' ? r : substring;
20+
return isDefined(r) ? r : substring;
1921
});
2022
}
2123

@@ -31,16 +33,16 @@ export class Parser {
3133
key = '';
3234
do {
3335
key += keys.shift();
34-
if (target!==undefined && target[key] !== undefined && (typeof target[key] === 'object' || !keys.length)) {
36+
if(isDefined(target) && isDefined(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
3537
target = target[key];
3638
key = '';
37-
} else if (!keys.length) {
39+
} else if(!keys.length) {
3840
target = undefined;
3941
} else {
4042
key += '.';
4143
}
42-
} while (keys.length);
43-
44+
} while(keys.length);
45+
4446
return target;
4547
}
4648

src/translate.pipe.ts

Lines changed: 4 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {PipeTransform, Pipe, Injectable, EventEmitter, OnDestroy, ChangeDetectorRef} from '@angular/core';
22
import {TranslateService, LangChangeEvent, TranslationChangeEvent} from './translate.service';
3+
import {equals, isDefined} from "./util";
34

45
@Injectable()
56
@Pipe({
@@ -16,60 +17,6 @@ export class TranslatePipe implements PipeTransform, OnDestroy {
1617
constructor(private translate: TranslateService, private _ref: ChangeDetectorRef) {
1718
}
1819

19-
/* tslint:disable */
20-
/**
21-
* @name equals
22-
*
23-
* @description
24-
* Determines if two objects or two values are equivalent.
25-
*
26-
* Two objects or values are considered equivalent if at least one of the following is true:
27-
*
28-
* * Both objects or values pass `===` comparison.
29-
* * Both objects or values are of the same type and all of their properties are equal by
30-
* comparing them with `equals`.
31-
*
32-
* @param {*} o1 Object or value to compare.
33-
* @param {*} o2 Object or value to compare.
34-
* @returns {boolean} True if arguments are equal.
35-
*/
36-
private equals(o1: any, o2: any): boolean {
37-
if(o1 === o2) return true;
38-
if(o1 === null || o2 === null) return false;
39-
if(o1 !== o1 && o2 !== o2) return true; // NaN === NaN
40-
let t1 = typeof o1, t2 = typeof o2, length: number, key: any, keySet: any;
41-
if(t1 == t2 && t1 == 'object') {
42-
if(Array.isArray(o1)) {
43-
if(!Array.isArray(o2)) return false;
44-
if((length = o1.length) == o2.length) {
45-
for(key = 0; key < length; key++) {
46-
if(!this.equals(o1[key], o2[key])) return false;
47-
}
48-
return true;
49-
}
50-
} else {
51-
if(Array.isArray(o2)) {
52-
return false;
53-
}
54-
keySet = Object.create(null);
55-
for(key in o1) {
56-
if(!this.equals(o1[key], o2[key])) {
57-
return false;
58-
}
59-
keySet[key] = true;
60-
}
61-
for(key in o2) {
62-
if(!(key in keySet) && typeof o2[key] !== 'undefined') {
63-
return false;
64-
}
65-
}
66-
return true;
67-
}
68-
}
69-
return false;
70-
}
71-
/* tslint:enable */
72-
7320
updateValue(key: string, interpolateParams?: Object, translations?: any): void {
7421
let onTranslation = (res: string) => {
7522
this.value = res !== undefined ? res : key;
@@ -91,13 +38,14 @@ export class TranslatePipe implements PipeTransform, OnDestroy {
9138
if(!query || query.length === 0) {
9239
return query;
9340
}
41+
9442
// if we ask another time for the same key, return the last value
95-
if(this.equals(query, this.lastKey) && this.equals(args, this.lastParams)) {
43+
if(equals(query, this.lastKey) && equals(args, this.lastParams)) {
9644
return this.value;
9745
}
9846

9947
let interpolateParams: Object;
100-
if(args.length && args[0] !== null) {
48+
if(isDefined(args[0]) && args.length) {
10149
if(typeof args[0] === 'string' && args[0].length) {
10250
// we accept objects written in the template such as {n:1}, {'n':1}, {n:'v'}
10351
// which is why we might need to change it to real JSON objects such as {"n":1} or {"n":"v"}

src/translate.service.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import "rxjs/add/operator/merge";
99
import "rxjs/add/operator/toArray";
1010

1111
import {Parser} from "./translate.parser";
12+
import {isDefined} from "./util";
1213

1314
export interface TranslationChangeEvent {
1415
translations: any;
@@ -46,7 +47,7 @@ export interface MissingTranslationHandlerParams {
4647
declare interface Window {
4748
navigator: any;
4849
}
49-
declare var window: Window;
50+
declare const window: Window;
5051

5152
export abstract class MissingTranslationHandler {
5253
/**
@@ -294,7 +295,7 @@ export class TranslateService {
294295
* @returns {any} the translated key, or an object of translated keys
295296
*/
296297
public get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> {
297-
if(!key) {
298+
if(!isDefined(key) || !key.length) {
298299
throw new Error(`Parameter "key" required`);
299300
}
300301
// check if we are loading a new translation to use
@@ -334,7 +335,7 @@ export class TranslateService {
334335
* @returns {string}
335336
*/
336337
public instant(key: string|Array<string>, interpolateParams?: Object): string|any {
337-
if(!key) {
338+
if(!isDefined(key) || !key.length) {
338339
throw new Error(`Parameter "key" required`);
339340
}
340341

0 commit comments

Comments
 (0)