diff --git a/angular2-translator/TranslateLoaderJson.ts b/angular2-translator/TranslateLoaderJson.ts index d852d18..59be580 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 @@ -36,19 +36,8 @@ export class TranslateLoaderJson extends TranslateLoader { .subscribe( (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]; - } - } + let translations = {}; + this.flattenTranslations(translations, response.json()); resolve(translations); } else { reject("StatusCode: " + response.status + ""); @@ -60,4 +49,16 @@ export class TranslateLoaderJson extends TranslateLoader { ); }); } + + 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]; + } + } + } } diff --git a/docs/TranslateLoaderJson.md b/docs/TranslateLoaderJson.md index b64c7ed..2b9ba73 100644 --- a/docs/TranslateLoaderJson.md +++ b/docs/TranslateLoaderJson.md @@ -22,6 +22,43 @@ To keep order in your translation file your can use arrays for translations. Exa } ``` +## Nested translation tables + +For more structure in your translation file we allow objects. Please note that they are merged to one dimension. + +```json +{ + "app": { + "loginText": "Please login before continuing!", + "componentA": { + "TEXT": "something else" + } + } +} +``` + +The translation table becomes: + +```json +{ + "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" + }, + "TEXT": "something else" +} +``` + ## 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..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({ @@ -165,7 +165,143 @@ 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({"TEXT.NESTED": "This is a text"}); + }); + + 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({"TEXT.NESTED": "This is a text", "TEXT.SECONDNEST.TEXT": "Second text"}); + }); + + 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("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({"text.nestedText": "This is a text"}); + }); + + it("filters non string values within nested object", function () { + let promise = loader.load("en"); + let nestedObj: any = { + TEXT: { + ANSWER: 42, + NESTED: "This is a text", + }, + }; + + 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("merges translations to one dimension", function () { + let promise = loader.load("en"); + + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify({ + app: { + componentA: { + TEXT: "something else", + }, + loginText: "Please login before continuing!", + }, + }), + status: 200, + }))); + + expect(promise).toBeResolvedWith({ + "app.componentA.TEXT": "something else", + "app.loginText": "Please login before continuing!", + }); + }); + + it("filters non string values", function () { let promise = loader.load("en"); connection.mockRespond(new Response(new ResponseOptions({ 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({