Skip to content

Commit

Permalink
fix(TranslateService): shouldMerge option now does deep merging
Browse files Browse the repository at this point in the history
BREAKING CHANGE: shouldMerge option of the `setTranslation` method was only doing shallow merging using Object.assign. It is now doing deep merging of objects and arrays, which is what was initially intended (hence the name).

Fixes #532
  • Loading branch information
ocombe committed May 24, 2017
1 parent 647c2d7 commit 7a609e0
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 8 deletions.
4 changes: 2 additions & 2 deletions src/translate.service.ts
Expand Up @@ -14,7 +14,7 @@ import {TranslateStore} from "./translate.store";
import {TranslateLoader} from "./translate.loader";
import {MissingTranslationHandler, MissingTranslationHandlerParams} from "./missing-translation-handler";
import {TranslateParser} from "./translate.parser";
import {isDefined} from "./util";
import {deepMerge, isDefined} from "./util";

export const USE_STORE = new OpaqueToken('USE_STORE');

Expand Down Expand Up @@ -269,7 +269,7 @@ export class TranslateService {
*/
public setTranslation(lang: string, translations: Object, shouldMerge: boolean = false): void {
if(shouldMerge && this.translations[lang]) {
Object.assign(this.translations[lang], translations);
this.translations[lang] = deepMerge(this.translations[lang], translations);
} else {
this.translations[lang] = translations;
}
Expand Down
70 changes: 69 additions & 1 deletion src/util.ts
Expand Up @@ -54,4 +54,72 @@ export function equals(o1: any, o2: any): boolean {

export function isDefined(value: any): boolean {
return typeof value !== 'undefined' && value !== null;
}
}

/** Start deep merge, from https://github.com/KyleAMathews/deepmerge **/
function isMergeableObject<T>(val: T): boolean {
let nonNullObject = val && typeof val === 'object';

return nonNullObject
&& Object.prototype.toString.call(val) !== '[object RegExp]'
&& Object.prototype.toString.call(val) !== '[object Date]';
}

function emptyTarget<T>(val: T): any {
return Array.isArray(val) ? [] : {};
}

function cloneIfNecessary<T>(value: T, options: DeepMergeOptions<T>): T {
let clone = options && options.clone === true;
return clone && isMergeableObject(value) ? deepMerge(emptyTarget(value), value, options) : value;
}

function defaultArrayMerge<T>(target: T[], source: T[], options: DeepMergeOptions<T>): T[] {
let destination = target.slice();
source.forEach(function(e, i) {
if(typeof destination[i] === 'undefined') {
destination[i] = cloneIfNecessary(e, options);
} else if(isMergeableObject(e)) {
destination[i] = deepMerge(target[i], e, options);
} else if(target.indexOf(e) === -1) {
destination.push(cloneIfNecessary(e, options));
}
});
return destination;
}

function mergeObject<T>(target: any, source: any, options: DeepMergeOptions<T>): T {
let destination: any = {};
if(isMergeableObject(target)) {
Object.keys(target).forEach(function(key) {
destination[key] = cloneIfNecessary(target[key], options)
});
}
Object.keys(source).forEach(function(key) {
if(!isMergeableObject(source[key]) || !target[key]) {
destination[key] = cloneIfNecessary(source[key], options);
} else {
destination[key] = deepMerge(target[key], source[key], options);
}
});
return destination;
}

export interface DeepMergeOptions<T> {
clone?: boolean;

arrayMerge?(destination: T, source: T, options?: DeepMergeOptions<T>): T;
}

export function deepMerge<T>(target: T, source: T, options?: DeepMergeOptions<T>): T {
let array = Array.isArray(source);
options = options || {arrayMerge: defaultArrayMerge} as any;
let arrayMerge: any = options.arrayMerge || defaultArrayMerge;

if(array) {
return Array.isArray(target) ? arrayMerge(target, source, options) : cloneIfNecessary(source, options);
} else {
return mergeObject(target, source, options);
}
}
/** End deep merge **/
10 changes: 5 additions & 5 deletions tests/translate.service.spec.ts
Expand Up @@ -172,14 +172,14 @@ describe('TranslateService', () => {
});
});

it("shouldn't override the translations if you set the translations twice", (done: Function) => {
it("should merge translations if option shouldMerge is true", (done: Function) => {
translations = {};
translate.setTranslation('en', {"TEST": "This is a test"}, true);
translate.setTranslation('en', {"TEST2": "This is a test"}, true);
translate.setTranslation('en', {"TEST": {"sub1": "value1"}}, true);
translate.setTranslation('en', {"TEST": {"sub2": "value2"}}, true);
translate.use('en');

translate.get('TEST').subscribe((res: string) => {
expect(res).toEqual('This is a test');
translate.get('TEST').subscribe((res: any) => {
expect(res).toEqual({"sub1": "value1", "sub2": "value2"});
expect(translations).toEqual({});
done();
});
Expand Down

0 comments on commit 7a609e0

Please sign in to comment.