From b4128cee66548dea4b9c30ae6561f01025883fa3 Mon Sep 17 00:00:00 2001 From: BorntraegerMarc Date: Fri, 23 Dec 2016 11:14:29 +0100 Subject: [PATCH 1/2] Nested objects are now allowed for translations. --- angular2-translator/TranslateLoaderJson.ts | 30 +++-- docs/TranslateLoaderJson.md | 18 +++ tests/TranslateLoaderJson.spec.ts | 150 ++++++++++++++++++++- 3 files changed, 184 insertions(+), 14 deletions(-) diff --git a/angular2-translator/TranslateLoaderJson.ts b/angular2-translator/TranslateLoaderJson.ts index d852d18..1e600e4 100644 --- a/angular2-translator/TranslateLoaderJson.ts +++ b/angular2-translator/TranslateLoaderJson.ts @@ -3,7 +3,7 @@ import {Inject, Injectable} from "@angular/core"; import {Http} from "@angular/http"; export class TranslateLoaderJsonConfig { - public path: string = "i18n/"; + public path: string = "i18n/"; public extension: string = ".json"; // @todo maybe we will change it to a destructed parameter like we did for TranslateConfig @@ -37,18 +37,7 @@ export class TranslateLoaderJson extends TranslateLoader { (response) => { if (response.status === 200) { let translations = response.json(); - let key; - for (key in translations) { - if (Array.isArray(translations[key])) { - - translations[key] = translations[key] - .filter((v) => typeof v === "string") - .join(""); - - } else if (typeof translations[key] !== "string") { - delete translations[key]; - } - } + translations = this.filter(translations); resolve(translations); } else { reject("StatusCode: " + response.status + ""); @@ -60,4 +49,19 @@ export class TranslateLoaderJson extends TranslateLoader { ); }); } + + private filter(translations): any { + for (let key in translations) { + if (typeof translations[key] == "object" && translations[key] !== null && !Array.isArray(translations[key])) { + this.filter(translations[key]); + } else if (Array.isArray(translations[key])) { + translations[key] = translations[key] + .filter((v) => typeof v === "string") + .join(""); + } else if (typeof translations[key] !== "string") { + delete translations[key]; + } + } + return translations; + } } diff --git a/docs/TranslateLoaderJson.md b/docs/TranslateLoaderJson.md index b64c7ed..4546b45 100644 --- a/docs/TranslateLoaderJson.md +++ b/docs/TranslateLoaderJson.md @@ -22,6 +22,24 @@ To keep order in your translation file your can use arrays for translations. Exa } ``` +## Filters + +For convenience this loader will automatically filter out every translation key, which is not ether a object, array or string. + +Multiple nestings are allowed. For example: + +```json +{ + "TEXT": { + "NESTED": "This is a text" + }, + "COOKIE_INFORMATION": [ + "We are using cookies to adjust our website to the needs of our customers. ", + "By using our websites you agree to store cookies on your computer, tablet or smartphone.", + ] +} +``` + ## TranslateLoaderJsonConfig To configure TranslateLoaderJson you can create your own TranslateLoaderJsonConfig and provide it. diff --git a/tests/TranslateLoaderJson.spec.ts b/tests/TranslateLoaderJson.spec.ts index 6dbbc9f..9159df8 100644 --- a/tests/TranslateLoaderJson.spec.ts +++ b/tests/TranslateLoaderJson.spec.ts @@ -165,7 +165,155 @@ describe("TranslateLoaderJson", function () { }); }); - it("filters non string values", function() { + it("allows nested objects", function () { + let promise = loader.load("en"); + let nestedObj: any = { + TEXT: { + NESTED: "This is a text" + } + }; + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(nestedObj), + status: 200, + }))); + + expect(promise).toBeResolvedWith(nestedObj); + }); + + it("allows multiple nested objects", function () { + let promise = loader.load("en"); + let nestedObj: any = { + TEXT: { + NESTED: "This is a text", + SECONDNEST: { + TEXT: "Second text" + } + } + }; + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(nestedObj), + status: 200, + }))); + + expect(promise).toBeResolvedWith(nestedObj); + }); + + it("combines arrays to a string while returning nested objects", function () { + let promise = loader.load("en"); + let nestedObj: any = { + COOKIE_INFORMATION: [ + "We are using cookies to adjust our website to the needs of our customers. ", + "By using our websites you agree to store cookies on your computer, tablet or smartphone.", + ], + TEXT: { + NESTED: "This is a text" + } + }; + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(nestedObj), + status: 200, + }))); + + expect(promise).toBeResolvedWith({ + COOKIE_INFORMATION: "We are using cookies to adjust our website to the needs of our customers. " + + "By using our websites you agree to store cookies on your computer, tablet or smartphone.", + TEXT: { + NESTED: "This is a text" + } + }); + }); + + it("returns nested objects and combines arrays when the object comes first", function () { + let promise = loader.load("en"); + let nestedObj: any = { + TEXT: { + NESTED: "This is a text" + }, + COOKIE_INFORMATION: [ + "We are using cookies to adjust our website to the needs of our customers. ", + "By using our websites you agree to store cookies on your computer, tablet or smartphone.", + ] + }; + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(nestedObj), + status: 200, + }))); + + expect(promise).toBeResolvedWith({ + TEXT: { + NESTED: "This is a text" + }, + COOKIE_INFORMATION: "We are using cookies to adjust our website to the needs of our customers. " + + "By using our websites you agree to store cookies on your computer, tablet or smartphone." + }); + }); + + it("allows nested objects with lower case keys and with camel case", function () { + let promise = loader.load("en"); + let nestedObj: any = { + text: { + nestedText: "This is a text" + } + }; + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(nestedObj), + status: 200, + }))); + + expect(promise).toBeResolvedWith(nestedObj); + }); + + it("filters non string values within nested object", function () { + let promise = loader.load("en"); + let nestedObj: any = { + TEXT: { + NESTED: "This is a text", + ANSWER: 42 + } + }; + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(nestedObj), + status: 200, + }))); + + expect(promise).toBeResolvedWith({ + TEXT: { + NESTED: "This is a text" + } + }); + }); + + it("combines arrays to a string while beeing in nested objects", function () { + let promise = loader.load("en"); + let nestedObj: any = { + TEXT: { + COOKIE_INFORMATION: [ + "We are using cookies to adjust our website to the needs of our customers. ", + "By using our websites you agree to store cookies on your computer, tablet or smartphone.", + ] + } + }; + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(nestedObj), + status: 200, + }))); + + expect(promise).toBeResolvedWith({ + TEXT: { + COOKIE_INFORMATION: "We are using cookies to adjust our website to the needs of our customers. " + + "By using our websites you agree to store cookies on your computer, tablet or smartphone." + } + }); + }); + + it("filters non string values", function () { let promise = loader.load("en"); connection.mockRespond(new Response(new ResponseOptions({ From ac18d24c883a232b95f28b686639b518cb72c87f Mon Sep 17 00:00:00 2001 From: BorntraegerMarc Date: Fri, 23 Dec 2016 11:52:22 +0100 Subject: [PATCH 2/2] Fixed CI build & nested objects are converted into one dimension objects. --- angular2-translator/TranslateLoaderJson.ts | 23 ++-- docs/TranslateLoaderJson.md | 37 ++++-- tests/TranslateLoaderJson.spec.ts | 126 ++++++++++----------- tests/TranslateService.spec.ts | 14 +++ 4 files changed, 109 insertions(+), 91 deletions(-) diff --git a/angular2-translator/TranslateLoaderJson.ts b/angular2-translator/TranslateLoaderJson.ts index 1e600e4..59be580 100644 --- a/angular2-translator/TranslateLoaderJson.ts +++ b/angular2-translator/TranslateLoaderJson.ts @@ -36,8 +36,8 @@ export class TranslateLoaderJson extends TranslateLoader { .subscribe( (response) => { if (response.status === 200) { - let translations = response.json(); - translations = this.filter(translations); + let translations = {}; + this.flattenTranslations(translations, response.json()); resolve(translations); } else { reject("StatusCode: " + response.status + ""); @@ -50,18 +50,15 @@ export class TranslateLoaderJson extends TranslateLoader { }); } - private filter(translations): any { - for (let key in translations) { - if (typeof translations[key] == "object" && translations[key] !== null && !Array.isArray(translations[key])) { - this.filter(translations[key]); - } else if (Array.isArray(translations[key])) { - translations[key] = translations[key] - .filter((v) => typeof v === "string") - .join(""); - } else if (typeof translations[key] !== "string") { - delete translations[key]; + private flattenTranslations(translations: any, data: any, prefix: string = "") { + for (let key in data) { + if (Array.isArray(data[key])) { + translations[prefix + key] = data[key].filter(v => typeof v === "string").join(""); + } else if (typeof data[key] === "object") { + this.flattenTranslations(translations, data[key], prefix + key + "."); + } else if (typeof data[key] === "string") { + translations[prefix + key] = data[key]; } } - return translations; } } diff --git a/docs/TranslateLoaderJson.md b/docs/TranslateLoaderJson.md index 4546b45..2b9ba73 100644 --- a/docs/TranslateLoaderJson.md +++ b/docs/TranslateLoaderJson.md @@ -22,21 +22,40 @@ To keep order in your translation file your can use arrays for translations. Exa } ``` -## Filters +## Nested translation tables -For convenience this loader will automatically filter out every translation key, which is not ether a object, array or string. +For more structure in your translation file we allow objects. Please note that they are merged to one dimension. -Multiple nestings are allowed. For example: +```json +{ + "app": { + "loginText": "Please login before continuing!", + "componentA": { + "TEXT": "something else" + } + } +} +``` + +The translation table becomes: ```json { - "TEXT": { - "NESTED": "This is a text" + "app.loginText": "Please login before continuing!", + "app.componentA.TEXT": "something else" +} +``` + +So you can access them with `translate('app.loginText')`. You need to refer to translations with full key too: + +```json +{ + "app": { + "A": "This gets \"something else\": [[ TEXT ]]", + "B": "This gets \"something\" [[ app.TEXT ]]", + "TEXT": "something" }, - "COOKIE_INFORMATION": [ - "We are using cookies to adjust our website to the needs of our customers. ", - "By using our websites you agree to store cookies on your computer, tablet or smartphone.", - ] + "TEXT": "something else" } ``` diff --git a/tests/TranslateLoaderJson.spec.ts b/tests/TranslateLoaderJson.spec.ts index 9159df8..167f3df 100644 --- a/tests/TranslateLoaderJson.spec.ts +++ b/tests/TranslateLoaderJson.spec.ts @@ -47,8 +47,8 @@ describe("TranslateLoaderJson", function () { describe("constructor", function () { it("requires a TranslateLoaderJsonConfig", function () { TestBed.configureTestingModule({ - imports: [ HttpModule ], - providers: [ TranslateLoaderJson ], + imports: [HttpModule], + providers: [TranslateLoaderJson], }); let action = function () { @@ -66,16 +66,16 @@ describe("TranslateLoaderJson", function () { beforeEach(function () { TestBed.configureTestingModule({ - imports: [ HttpModule ], + imports: [HttpModule], providers: [ - { provide: XHRBackend, useClass: MockBackend }, - { provide: TranslateLoaderJsonConfig, useValue: new TranslateLoaderJsonConfig() }, + {provide: XHRBackend, useClass: MockBackend}, + {provide: TranslateLoaderJsonConfig, useValue: new TranslateLoaderJsonConfig()}, TranslateLoaderJson, ], }); - backend = TestBed.get(XHRBackend); - loader = TestBed.get(TranslateLoaderJson); + backend = TestBed.get(XHRBackend); + loader = TestBed.get(TranslateLoaderJson); backend.connections.subscribe((c: MockConnection) => connection = c); PromiseMatcher.install(); @@ -105,29 +105,29 @@ describe("TranslateLoaderJson", function () { expect(request.method).toBe(RequestMethod.Get); }); - it("resolves when connection responds", function() { + it("resolves when connection responds", function () { let promise = loader.load("en"); connection.mockRespond(new Response(new ResponseOptions({ - body: JSON.stringify({ TEXT: "This is a text" }), + body: JSON.stringify({TEXT: "This is a text"}), status: 200, }))); expect(promise).toBeResolved(); }); - it("transforms result to object", function() { + it("transforms result to object", function () { let promise = loader.load("en"); connection.mockRespond(new Response(new ResponseOptions({ - body: JSON.stringify({ TEXT: "This is a text" }), + body: JSON.stringify({TEXT: "This is a text"}), status: 200, }))); - expect(promise).toBeResolvedWith({ TEXT: "This is a text" }); + expect(promise).toBeResolvedWith({TEXT: "This is a text"}); }); - it("rejectes when connection fails", function() { + it("rejectes when connection fails", function () { let promise = loader.load("en"); connection.mockRespond(new Response(new ResponseOptions({ @@ -138,7 +138,7 @@ describe("TranslateLoaderJson", function () { expect(promise).toBeRejectedWith("StatusCode: 500"); }); - it("rejects when connection throws", function() { + it("rejects when connection throws", function () { let promise = loader.load("en"); connection.mockError(new Error("Some reason")); @@ -146,7 +146,7 @@ describe("TranslateLoaderJson", function () { expect(promise).toBeRejectedWith("Some reason"); }); - it("combines arrays to a string", function() { + it("combines arrays to a string", function () { let promise = loader.load("en"); connection.mockRespond(new Response(new ResponseOptions({ @@ -169,8 +169,8 @@ describe("TranslateLoaderJson", function () { let promise = loader.load("en"); let nestedObj: any = { TEXT: { - NESTED: "This is a text" - } + NESTED: "This is a text", + }, }; connection.mockRespond(new Response(new ResponseOptions({ @@ -178,7 +178,7 @@ describe("TranslateLoaderJson", function () { status: 200, }))); - expect(promise).toBeResolvedWith(nestedObj); + expect(promise).toBeResolvedWith({"TEXT.NESTED": "This is a text"}); }); it("allows multiple nested objects", function () { @@ -187,9 +187,9 @@ describe("TranslateLoaderJson", function () { TEXT: { NESTED: "This is a text", SECONDNEST: { - TEXT: "Second text" - } - } + TEXT: "Second text", + }, + }, }; connection.mockRespond(new Response(new ResponseOptions({ @@ -197,7 +197,7 @@ describe("TranslateLoaderJson", function () { status: 200, }))); - expect(promise).toBeResolvedWith(nestedObj); + expect(promise).toBeResolvedWith({"TEXT.NESTED": "This is a text", "TEXT.SECONDNEST.TEXT": "Second text"}); }); it("combines arrays to a string while returning nested objects", function () { @@ -208,8 +208,8 @@ describe("TranslateLoaderJson", function () { "By using our websites you agree to store cookies on your computer, tablet or smartphone.", ], TEXT: { - NESTED: "This is a text" - } + NESTED: "This is a text", + }, }; connection.mockRespond(new Response(new ResponseOptions({ @@ -218,24 +218,18 @@ describe("TranslateLoaderJson", function () { }))); expect(promise).toBeResolvedWith({ - COOKIE_INFORMATION: "We are using cookies to adjust our website to the needs of our customers. " + - "By using our websites you agree to store cookies on your computer, tablet or smartphone.", - TEXT: { - NESTED: "This is a text" - } + COOKIE_INFORMATION: "We are using cookies to adjust our website to " + + "the needs of our customers. By using our websites you agree to store cookies on your computer, " + + "tablet or smartphone.", "TEXT.NESTED": "This is a text", }); }); - it("returns nested objects and combines arrays when the object comes first", function () { + it("allows nested objects with lower case keys and with camel case", function () { let promise = loader.load("en"); let nestedObj: any = { - TEXT: { - NESTED: "This is a text" + text: { + nestedText: "This is a text", }, - COOKIE_INFORMATION: [ - "We are using cookies to adjust our website to the needs of our customers. ", - "By using our websites you agree to store cookies on your computer, tablet or smartphone.", - ] }; connection.mockRespond(new Response(new ResponseOptions({ @@ -243,21 +237,16 @@ describe("TranslateLoaderJson", function () { status: 200, }))); - expect(promise).toBeResolvedWith({ - TEXT: { - NESTED: "This is a text" - }, - COOKIE_INFORMATION: "We are using cookies to adjust our website to the needs of our customers. " + - "By using our websites you agree to store cookies on your computer, tablet or smartphone." - }); + expect(promise).toBeResolvedWith({"text.nestedText": "This is a text"}); }); - it("allows nested objects with lower case keys and with camel case", function () { + it("filters non string values within nested object", function () { let promise = loader.load("en"); let nestedObj: any = { - text: { - nestedText: "This is a text" - } + TEXT: { + ANSWER: 42, + NESTED: "This is a text", + }, }; connection.mockRespond(new Response(new ResponseOptions({ @@ -265,16 +254,18 @@ describe("TranslateLoaderJson", function () { status: 200, }))); - expect(promise).toBeResolvedWith(nestedObj); + expect(promise).toBeResolvedWith({"TEXT.NESTED": "This is a text"}); }); - it("filters non string values within nested object", function () { + it("combines arrays to a string while beeing in nested objects", function () { let promise = loader.load("en"); let nestedObj: any = { TEXT: { - NESTED: "This is a text", - ANSWER: 42 - } + COOKIE_INFORMATION: [ + "We are using cookies to adjust our website to the needs of our customers. ", + "By using our websites you agree to store cookies on your computer, tablet or smartphone.", + ], + }, }; connection.mockRespond(new Response(new ResponseOptions({ @@ -283,33 +274,30 @@ describe("TranslateLoaderJson", function () { }))); expect(promise).toBeResolvedWith({ - TEXT: { - NESTED: "This is a text" - } + "TEXT.COOKIE_INFORMATION": "We are using cookies to adjust our website " + + "to the needs of our customers. By using our websites you agree to store cookies on your " + + "computer, tablet or smartphone.", }); }); - it("combines arrays to a string while beeing in nested objects", function () { + it("merges translations to one dimension", function () { let promise = loader.load("en"); - let nestedObj: any = { - TEXT: { - COOKIE_INFORMATION: [ - "We are using cookies to adjust our website to the needs of our customers. ", - "By using our websites you agree to store cookies on your computer, tablet or smartphone.", - ] - } - }; connection.mockRespond(new Response(new ResponseOptions({ - body: JSON.stringify(nestedObj), + body: JSON.stringify({ + app: { + componentA: { + TEXT: "something else", + }, + loginText: "Please login before continuing!", + }, + }), status: 200, }))); expect(promise).toBeResolvedWith({ - TEXT: { - COOKIE_INFORMATION: "We are using cookies to adjust our website to the needs of our customers. " + - "By using our websites you agree to store cookies on your computer, tablet or smartphone." - } + "app.componentA.TEXT": "something else", + "app.loginText": "Please login before continuing!", }); }); diff --git a/tests/TranslateService.spec.ts b/tests/TranslateService.spec.ts index 27db513..ca202c2 100644 --- a/tests/TranslateService.spec.ts +++ b/tests/TranslateService.spec.ts @@ -584,6 +584,20 @@ describe("TranslateService", function () { ); })); + it("allows dots in key", fakeAsync(function() { + translate.waitForTranslation(); + loaderPromiseResolve({ + HELLO: "Hello [[ app.WORLD ]]!", + "app.WORLD": "World", + }); + JasminePromise.flush(); + spyOn(translate.logHandler, "error").and.callFake(() => {}); + + let translation = translate.instant("HELLO"); + + expect(translation).toBe("Hello World!"); + })); + it("key is finish after space character", fakeAsync(function() { translate.waitForTranslation(); loaderPromiseResolve({