Skip to content

Commit d50649f

Browse files
committed
feat(TranslateParser): You can now use nested keys for translations
Fixes #15
1 parent d9b3887 commit d50649f

File tree

6 files changed

+145
-61
lines changed

6 files changed

+145
-61
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,5 @@ With the given translation: `"HELLO_WORLD": "hello {{value}}"`.
123123
- `interpolate(expr: string, params?: any): string`: Interpolates a string to replace parameters.
124124

125125
`This is a {{ key }}` ==> `This is a value` with `params = { key: "value" }`
126+
- `flattenObject(target: Object): Object`: Flattens an object
127+
`{ key1: { keyA: 'valueI' }}` ==> `{ 'key1.keyA': 'valueI' }`

package.json

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"description": "An implementation of angular translate for Angular 2",
44
"scripts": {
55
"test": "tsc && karma start",
6+
"test-watch": "tsc && karma start --no-single-run --auto-watch",
67
"commit": "git-cz",
78
"prepublish": "tsc",
89
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
@@ -25,12 +26,12 @@
2526
"main": "ng2-translate.js",
2627
"typings": "./ng2-translate.d.ts",
2728
"homepage": "https://github.com/ocombe/ng2-translate",
28-
"dependencies": {
29+
"peerDependencies": {
2930
"angular2": "~2.0.0-beta.0",
30-
"es6-promise": "^3.0.2",
31-
"es6-shim": "^0.33.3",
32-
"reflect-metadata": "0.1.2",
33-
"rxjs": "5.0.0-beta.0",
31+
"es6-promise": "~3.0.2",
32+
"es6-shim": "~0.33.3",
33+
"reflect-metadata": "~0.1.2",
34+
"rxjs": "~5.0.0-beta.0",
3435
"zone.js": "~0.5.10"
3536
},
3637
"devDependencies": {
@@ -44,7 +45,14 @@
4445
"karma-typescript-preprocessor": "0.0.21",
4546
"semantic-release": "~4.3.5",
4647
"systemjs": "~0.19.6",
47-
"typescript": "~1.7.3"
48+
"typescript": "~1.7.3",
49+
50+
"angular2": "~2.0.0-beta.0",
51+
"es6-promise": "~3.0.2",
52+
"es6-shim": "~0.33.3",
53+
"reflect-metadata": "~0.1.2",
54+
"rxjs": "~5.0.0-beta.0",
55+
"zone.js": "~0.5.10"
4856
},
4957
"czConfig": {
5058
"path": "node_modules/cz-conventional-changelog"

src/translate.parser.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ export class Parser {
99
* @param params
1010
* @returns {string}
1111
*/
12-
interpolate(expr: string, params?: any): string {
13-
if(!params) {
14-
return expr;
15-
} else {
16-
params = this.flattenObject(params);
17-
}
18-
19-
return expr.replace(this.templateMatcher, function (substring: string, b: string): string {
20-
var r = params[b];
21-
return typeof r !== 'undefined' ? r : substring;
22-
});
12+
public interpolate(expr: string, params?: any): string {
13+
if(!params) {
14+
return expr;
15+
} else {
16+
params = this.flattenObject(params);
17+
}
18+
19+
return expr.replace(this.templateMatcher, function (substring: string, b: string): string {
20+
var r = params[b];
21+
return typeof r !== 'undefined' ? r : substring;
22+
});
2323
}
2424

2525
/**
@@ -28,7 +28,7 @@ export class Parser {
2828
* @param target
2929
* @returns {Object}
3030
*/
31-
private flattenObject(target: Object): Object {
31+
public flattenObject(target: Object): Object {
3232
var delimiter = '.';
3333
var maxDepth: number;
3434
var currentDepth = 1;

src/translate.service.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -160,33 +160,30 @@ export class TranslateService {
160160
* @returns {any} the translated key, or an object of translated keys
161161
*/
162162
public get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> {
163+
if(!key) {
164+
throw new Error('Parameter "key" required');
165+
}
166+
var getParsedResult = (translations: any, key: any) => {
167+
if(!translations) {
168+
return key;
169+
}
170+
if(key instanceof Array) {
171+
let result: any = {};
172+
for (var k of key) {
173+
result[k] = getParsedResult(translations, k);
174+
}
175+
return result;
176+
}
177+
return this.parser.interpolate(translations[key], interpolateParams) || key
178+
};
163179
// check if we are loading a new translation to use
164180
if(this.pending) {
165181
return this.pending.map((res: any) => {
166-
var result: any,
167-
getTranslation = (key: any) => this.parser.interpolate(res[key], interpolateParams) || key;
168-
if(key instanceof Array) {
169-
result = {};
170-
for (var k of key) {
171-
result[k] = getTranslation(k);
172-
}
173-
} else {
174-
result = getTranslation(key);
175-
}
176-
return result;
182+
return getParsedResult(this.parser.flattenObject(res), key);
177183
});
178184
} else {
179-
var result: any,
180-
getTranslation = (key: any) => this.translations && this.translations[this.currentLang] ? this.parser.interpolate(this.translations[this.currentLang][key], interpolateParams) : key || key;
181-
if(key instanceof Array) {
182-
result = {};
183-
for (var k of key) {
184-
result[k] = getTranslation(k);
185-
}
186-
} else {
187-
result = getTranslation(key);
188-
}
189-
return Observable.of(result);
185+
let translations = this.parser.flattenObject(this.translations[this.currentLang]);
186+
return Observable.of(getParsedResult(translations, key));
190187
}
191188
}
192189

tests/translate.parser.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,10 @@ export function main() {
2020
expect(parser.interpolate("This is a {{ key1.key2 }}", {key1: {key2: "value2"}})).toEqual("This is a value2");
2121
expect(parser.interpolate("This is a {{ key1.key2.key3 }}", {key1: {key2: {key3: "value3"}}})).toEqual("This is a value3");
2222
});
23+
24+
it('should be able to flatten objects', () => {
25+
expect(parser.flattenObject({key1: {key2: "value2"}})).toEqual({"key1.key2": "value2"});
26+
expect(parser.flattenObject({key1: {key2: {key3: "value3"}}})).toEqual({"key1.key2.key3": "value3"});
27+
});
2328
});
2429
}

tests/translate.service.spec.ts

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {it, beforeEachProviders, inject} from "angular2/testing";
2-
import {provide} from "angular2/core";
2+
import {provide, Injector} from "angular2/core";
33
import {
4-
BaseRequestOptions, Http, ResponseOptions, Response, HTTP_PROVIDERS, Connection,
4+
ResponseOptions, Response, HTTP_PROVIDERS, Connection,
55
XHRBackend
66
} from "angular2/http";
77
import {MockBackend, MockConnection} from "angular2/http/testing";
@@ -10,37 +10,109 @@ import {TranslateService} from '../src/translate.service';
1010
export function main() {
1111

1212
describe('TranslateService', () => {
13-
beforeEachProviders(() => [
14-
BaseRequestOptions,
15-
HTTP_PROVIDERS,
16-
// Provide a mocked (fake) backend for Http
17-
provide(XHRBackend, {useClass: MockBackend}),
18-
TranslateService
19-
]);
20-
21-
22-
it('is defined', () => {
23-
expect(TranslateService).toBeDefined();
24-
});
25-
26-
// this test is async, and yet it works thanks to Zone \o/
27-
it('should be able to get translations for the view', inject([XHRBackend, Http, TranslateService], (xhrBackend, http, translate) => {
28-
var connection: MockConnection; //this will be set when a new connection is emitted from the backend.
29-
xhrBackend.connections.subscribe((c: MockConnection) => connection = c);
13+
let injector: Injector;
14+
let backend: MockBackend;
15+
let translate: TranslateService;
16+
var connection: MockConnection; // this will be set when a new connection is emitted from the backend.
3017

18+
var prepareStaticTranslate = () => {
3119
// this will load translate json files from src/public/i18n
3220
translate.useStaticFilesLoader();
3321

3422
// the lang to use, if the lang isn't available, it will use the current loader to get them
3523
translate.use('en');
24+
};
25+
26+
var mockBackendResponse = (response: string) => {
27+
connection.mockRespond(new Response(new ResponseOptions({body: response})));
28+
};
29+
30+
beforeEach(() => {
31+
injector = Injector.resolveAndCreate([
32+
HTTP_PROVIDERS,
33+
// Provide a mocked (fake) backend for Http
34+
provide(XHRBackend, {useClass: MockBackend}),
35+
TranslateService
36+
]);
37+
backend = injector.get(XHRBackend);
38+
translate = injector.get(TranslateService);
39+
// sets the connection when someone tries to access the backend with an xhr request
40+
backend.connections.subscribe((c: MockConnection) => connection = c);
41+
});
42+
43+
it('is defined', () => {
44+
expect(TranslateService).toBeDefined();
45+
expect(translate).toBeDefined();
46+
expect(translate instanceof TranslateService).toBeTruthy();
47+
});
48+
49+
it('should be able to get translations', () => {
50+
prepareStaticTranslate();
3651

3752
// this will request the translation from the backend because we use a static files loader for TranslateService
3853
translate.get('TEST').subscribe((res: string) => {
3954
expect(res).toEqual('This is a test');
4055
});
4156

4257
// mock response after the xhr request, otherwise it will be undefined
43-
connection.mockRespond(new Response(new ResponseOptions({body: '{"TEST": "This is a test"}'})));
44-
}));
58+
mockBackendResponse('{"TEST": "This is a test", "TEST2": "This is another test"}');
59+
60+
// this will request the translation from downloaded translations without making a request to the backend
61+
translate.get('TEST2').subscribe((res: string) => {
62+
expect(res).toEqual('This is another test');
63+
});
64+
});
65+
66+
it("should return the key when it doesn't find a translation", () => {
67+
prepareStaticTranslate();
68+
69+
translate.get('TEST').subscribe((res: string) => {
70+
expect(res).toEqual('TEST');
71+
});
72+
73+
mockBackendResponse('{}');
74+
});
75+
76+
it('should be able to get translations with params', () => {
77+
prepareStaticTranslate();
78+
79+
translate.get('TEST', {param: 'with param'}).subscribe((res: string) => {
80+
expect(res).toEqual('This is a test with param');
81+
});
82+
83+
mockBackendResponse('{"TEST": "This is a test {{param}}"}');
84+
});
85+
86+
it('should be able to get translations with nested params', () => {
87+
prepareStaticTranslate();
88+
89+
translate.get('TEST', {param: {value: 'with param'}}).subscribe((res: string) => {
90+
expect(res).toEqual('This is a test with param');
91+
});
92+
93+
mockBackendResponse('{"TEST": "This is a test {{param.value}}"}');
94+
});
95+
96+
it('should throw if you forget the key', () => {
97+
prepareStaticTranslate();
98+
99+
expect(() => {
100+
translate.get(undefined);
101+
}).toThrowError('Parameter "key" required');
102+
});
103+
104+
it('should be able to get translations with nested keys', () => {
105+
prepareStaticTranslate();
106+
107+
translate.get('TEST.TEST').subscribe((res: string) => {
108+
expect(res).toEqual('This is a test');
109+
});
110+
111+
mockBackendResponse('{"TEST": {"TEST": "This is a test"}, "TEST2": {"TEST2": {"TEST2": "This is another test"}}}');
112+
113+
translate.get('TEST2.TEST2.TEST2').subscribe((res: string) => {
114+
expect(res).toEqual('This is another test');
115+
});
116+
});
45117
});
46118
}

0 commit comments

Comments
 (0)