Skip to content

Commit

Permalink
feat(TranslateParser): You can now use nested keys for translations
Browse files Browse the repository at this point in the history
Fixes #15
  • Loading branch information
ocombe committed Jan 17, 2016
1 parent d9b3887 commit d50649f
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 61 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,5 @@ With the given translation: `"HELLO_WORLD": "hello {{value}}"`.
- `interpolate(expr: string, params?: any): string`: Interpolates a string to replace parameters.

`This is a {{ key }}` ==> `This is a value` with `params = { key: "value" }`
- `flattenObject(target: Object): Object`: Flattens an object
`{ key1: { keyA: 'valueI' }}` ==> `{ 'key1.keyA': 'valueI' }`
20 changes: 14 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"description": "An implementation of angular translate for Angular 2",
"scripts": {
"test": "tsc && karma start",
"test-watch": "tsc && karma start --no-single-run --auto-watch",
"commit": "git-cz",
"prepublish": "tsc",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
Expand All @@ -25,12 +26,12 @@
"main": "ng2-translate.js",
"typings": "./ng2-translate.d.ts",
"homepage": "https://github.com/ocombe/ng2-translate",
"dependencies": {
"peerDependencies": {
"angular2": "~2.0.0-beta.0",
"es6-promise": "^3.0.2",
"es6-shim": "^0.33.3",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.0",
"es6-promise": "~3.0.2",
"es6-shim": "~0.33.3",
"reflect-metadata": "~0.1.2",
"rxjs": "~5.0.0-beta.0",
"zone.js": "~0.5.10"
},
"devDependencies": {
Expand All @@ -44,7 +45,14 @@
"karma-typescript-preprocessor": "0.0.21",
"semantic-release": "~4.3.5",
"systemjs": "~0.19.6",
"typescript": "~1.7.3"
"typescript": "~1.7.3",

"angular2": "~2.0.0-beta.0",
"es6-promise": "~3.0.2",
"es6-shim": "~0.33.3",
"reflect-metadata": "~0.1.2",
"rxjs": "~5.0.0-beta.0",
"zone.js": "~0.5.10"
},
"czConfig": {
"path": "node_modules/cz-conventional-changelog"
Expand Down
24 changes: 12 additions & 12 deletions src/translate.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ export class Parser {
* @param params
* @returns {string}
*/
interpolate(expr: string, params?: any): string {
if(!params) {
return expr;
} else {
params = this.flattenObject(params);
}

return expr.replace(this.templateMatcher, function (substring: string, b: string): string {
var r = params[b];
return typeof r !== 'undefined' ? r : substring;
});
public interpolate(expr: string, params?: any): string {
if(!params) {
return expr;
} else {
params = this.flattenObject(params);
}

return expr.replace(this.templateMatcher, function (substring: string, b: string): string {
var r = params[b];
return typeof r !== 'undefined' ? r : substring;
});
}

/**
Expand All @@ -28,7 +28,7 @@ export class Parser {
* @param target
* @returns {Object}
*/
private flattenObject(target: Object): Object {
public flattenObject(target: Object): Object {
var delimiter = '.';
var maxDepth: number;
var currentDepth = 1;
Expand Down
41 changes: 19 additions & 22 deletions src/translate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,33 +160,30 @@ 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) {
throw new Error('Parameter "key" required');
}
var getParsedResult = (translations: any, key: any) => {
if(!translations) {
return key;
}
if(key instanceof Array) {
let result: any = {};
for (var k of key) {
result[k] = getParsedResult(translations, k);
}
return result;
}
return this.parser.interpolate(translations[key], interpolateParams) || key
};
// check if we are loading a new translation to use
if(this.pending) {
return this.pending.map((res: any) => {
var result: any,
getTranslation = (key: any) => this.parser.interpolate(res[key], interpolateParams) || key;
if(key instanceof Array) {
result = {};
for (var k of key) {
result[k] = getTranslation(k);
}
} else {
result = getTranslation(key);
}
return result;
return getParsedResult(this.parser.flattenObject(res), key);
});
} else {
var result: any,
getTranslation = (key: any) => this.translations && this.translations[this.currentLang] ? this.parser.interpolate(this.translations[this.currentLang][key], interpolateParams) : key || key;
if(key instanceof Array) {
result = {};
for (var k of key) {
result[k] = getTranslation(k);
}
} else {
result = getTranslation(key);
}
return Observable.of(result);
let translations = this.parser.flattenObject(this.translations[this.currentLang]);
return Observable.of(getParsedResult(translations, key));
}
}

Expand Down
5 changes: 5 additions & 0 deletions tests/translate.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,10 @@ export function main() {
expect(parser.interpolate("This is a {{ key1.key2 }}", {key1: {key2: "value2"}})).toEqual("This is a value2");
expect(parser.interpolate("This is a {{ key1.key2.key3 }}", {key1: {key2: {key3: "value3"}}})).toEqual("This is a value3");
});

it('should be able to flatten objects', () => {
expect(parser.flattenObject({key1: {key2: "value2"}})).toEqual({"key1.key2": "value2"});
expect(parser.flattenObject({key1: {key2: {key3: "value3"}}})).toEqual({"key1.key2.key3": "value3"});
});
});
}
114 changes: 93 additions & 21 deletions tests/translate.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {it, beforeEachProviders, inject} from "angular2/testing";
import {provide} from "angular2/core";
import {provide, Injector} from "angular2/core";
import {
BaseRequestOptions, Http, ResponseOptions, Response, HTTP_PROVIDERS, Connection,
ResponseOptions, Response, HTTP_PROVIDERS, Connection,
XHRBackend
} from "angular2/http";
import {MockBackend, MockConnection} from "angular2/http/testing";
Expand All @@ -10,37 +10,109 @@ import {TranslateService} from '../src/translate.service';
export function main() {

describe('TranslateService', () => {
beforeEachProviders(() => [
BaseRequestOptions,
HTTP_PROVIDERS,
// Provide a mocked (fake) backend for Http
provide(XHRBackend, {useClass: MockBackend}),
TranslateService
]);


it('is defined', () => {
expect(TranslateService).toBeDefined();
});

// this test is async, and yet it works thanks to Zone \o/
it('should be able to get translations for the view', inject([XHRBackend, Http, TranslateService], (xhrBackend, http, translate) => {
var connection: MockConnection; //this will be set when a new connection is emitted from the backend.
xhrBackend.connections.subscribe((c: MockConnection) => connection = c);
let injector: Injector;
let backend: MockBackend;
let translate: TranslateService;
var connection: MockConnection; // this will be set when a new connection is emitted from the backend.

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

// the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use('en');
};

var mockBackendResponse = (response: string) => {
connection.mockRespond(new Response(new ResponseOptions({body: response})));
};

beforeEach(() => {
injector = Injector.resolveAndCreate([
HTTP_PROVIDERS,
// Provide a mocked (fake) backend for Http
provide(XHRBackend, {useClass: MockBackend}),
TranslateService
]);
backend = injector.get(XHRBackend);
translate = injector.get(TranslateService);
// sets the connection when someone tries to access the backend with an xhr request
backend.connections.subscribe((c: MockConnection) => connection = c);
});

it('is defined', () => {
expect(TranslateService).toBeDefined();
expect(translate).toBeDefined();
expect(translate instanceof TranslateService).toBeTruthy();
});

it('should be able to get translations', () => {
prepareStaticTranslate();

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

// mock response after the xhr request, otherwise it will be undefined
connection.mockRespond(new Response(new ResponseOptions({body: '{"TEST": "This is a test"}'})));
}));
mockBackendResponse('{"TEST": "This is a test", "TEST2": "This is another test"}');

// this will request the translation from downloaded translations without making a request to the backend
translate.get('TEST2').subscribe((res: string) => {
expect(res).toEqual('This is another test');
});
});

it("should return the key when it doesn't find a translation", () => {
prepareStaticTranslate();

translate.get('TEST').subscribe((res: string) => {
expect(res).toEqual('TEST');
});

mockBackendResponse('{}');
});

it('should be able to get translations with params', () => {
prepareStaticTranslate();

translate.get('TEST', {param: 'with param'}).subscribe((res: string) => {
expect(res).toEqual('This is a test with param');
});

mockBackendResponse('{"TEST": "This is a test {{param}}"}');
});

it('should be able to get translations with nested params', () => {
prepareStaticTranslate();

translate.get('TEST', {param: {value: 'with param'}}).subscribe((res: string) => {
expect(res).toEqual('This is a test with param');
});

mockBackendResponse('{"TEST": "This is a test {{param.value}}"}');
});

it('should throw if you forget the key', () => {
prepareStaticTranslate();

expect(() => {
translate.get(undefined);
}).toThrowError('Parameter "key" required');
});

it('should be able to get translations with nested keys', () => {
prepareStaticTranslate();

translate.get('TEST.TEST').subscribe((res: string) => {
expect(res).toEqual('This is a test');
});

mockBackendResponse('{"TEST": {"TEST": "This is a test"}, "TEST2": {"TEST2": {"TEST2": "This is another test"}}}');

translate.get('TEST2.TEST2.TEST2').subscribe((res: string) => {
expect(res).toEqual('This is another test');
});
});
});
}

0 comments on commit d50649f

Please sign in to comment.