From 9ee6b147a79187a4452a0eb5420f1d742235f43a Mon Sep 17 00:00:00 2001 From: Juicetan Date: Fri, 30 May 2025 21:20:39 -0400 Subject: [PATCH 1/3] update utilities; --- src/utils/num.js | 10 ++++++++++ src/utils/obj.js | 9 +++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/utils/num.js diff --git a/src/utils/num.js b/src/utils/num.js new file mode 100644 index 0000000..8c86ac5 --- /dev/null +++ b/src/utils/num.js @@ -0,0 +1,10 @@ +var NumUtil = { + isNumber: function(val){ + return typeof val === 'number' && !isNaN(val); + }, + isStrNumber: function(val){ + return NumUtil.isNumber(+val); + } +}; + +export default NumUtil; \ No newline at end of file diff --git a/src/utils/obj.js b/src/utils/obj.js index fc25ac7..9bcdc54 100644 --- a/src/utils/obj.js +++ b/src/utils/obj.js @@ -1,4 +1,9 @@ +import NumUtil from "./num"; + var ObjUtil = { + isObject: function(obj){ + return typeof obj === 'object' && obj !== null; + }, getByStr: function (obj, keyStr) { // convert indexes to properties keyStr = keyStr.replace(/\[(\w+)\]/g, '.$1'); @@ -7,7 +12,7 @@ var ObjUtil = { var aKeys = keyStr.split('.'); for (var i = 0, n = aKeys.length; i < n; ++i) { var key = aKeys[i]; - if (key in obj) { + if (ObjUtil.isObject(obj) && key in obj) { obj = obj[key]; } else { return; @@ -26,7 +31,7 @@ var ObjUtil = { if (key in obj && i + 1 < n) { obj = obj[key]; } else if (!(key in obj) && i + 1 < n) { - obj[key] = {}; + obj[key] = NumUtil.isStrNumber(aKeys[i+1]) ? [] : {}; obj = obj[key]; } else { obj[key] = val; From 2784abcf7e745b171276c875208de5c2761ff98c Mon Sep 17 00:00:00 2001 From: Juicetan Date: Sat, 31 May 2025 05:35:35 -0400 Subject: [PATCH 2/3] add missing initialization for new attributes; --- src/models/localizedModel.js | 10 ++++++++-- tests/object.spec.js | 11 +++++++++++ tests/toplevel.spec.js | 10 ++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/models/localizedModel.js b/src/models/localizedModel.js index c4c7d77..73598ac 100644 --- a/src/models/localizedModel.js +++ b/src/models/localizedModel.js @@ -7,7 +7,10 @@ class LocalizedModel{ const model = this; this._localizedProxy = new Proxy(this, { set(target, prop, newVal){ - const attrMap = model._strMap[prop]; + var attrMap = model._strMap[prop]; + if(!attrMap && prop !== '_strMap' && prop !== '_localeKey' && prop !== '_localizedProxy'){ + attrMap = model._strMap[prop] = {}; + } if(attrMap){ model._setLocalizedValue(prop, newVal); } @@ -97,7 +100,10 @@ class LocalizedModel{ return new Proxy(object, { set(target, prop, newVal){ const resolvedAttrKey = `${attrKey}.${prop}`; - const attrMap = model._strMap[resolvedAttrKey]; + var attrMap = model._strMap[resolvedAttrKey]; + if(!attrMap && prop !== '_strMap' && prop !== '_localeKey' && prop !== '_localizedProxy'){ + attrMap = model._strMap[resolvedAttrKey] = {}; + } if(attrMap){ model._setLocalizedValue(resolvedAttrKey, newVal); } diff --git a/tests/object.spec.js b/tests/object.spec.js index 4cc389c..c09f72f 100644 --- a/tests/object.spec.js +++ b/tests/object.spec.js @@ -80,4 +80,15 @@ test('Update respective proxied locale values', () => { let serverPayload = appObj.toJSON(); expect(serverPayload?.secondLevel?.localizations?.en?.header).toBe('englishtest'); expect(serverPayload?.secondLevel?.localizations?.fr?.header).toBe('frenchtest'); +}) + +test('New instance setting attribute will serialize', () => { + let appObj = new AppObject(); + appObj.secondLevelObj.header = 'first value'; + appObj.localize('fr'); + appObj.secondLevelObj.header = 'french value'; + let serverPayload = appObj.toJSON(); + expect(serverPayload?.secondLevel).toBeInstanceOf(Object) + expect(serverPayload?.secondLevel?.localizations?.en?.header).toBe('first value') + expect(serverPayload?.secondLevel?.localizations?.fr?.header).toBe('french value') }) \ No newline at end of file diff --git a/tests/toplevel.spec.js b/tests/toplevel.spec.js index e854bfa..e94a1fb 100644 --- a/tests/toplevel.spec.js +++ b/tests/toplevel.spec.js @@ -78,4 +78,14 @@ test('Update respective proxied locale values', () => { let serverPayload = appObj.toJSON(); expect(serverPayload?.localizations?.en?.header).toBe('englishtest'); expect(serverPayload?.localizations?.fr?.header).toBe('frenchtest'); +}) + +test('New instance setting attribute will serialize', () => { + let appObj = new AppObject(); + appObj.header = 'first value'; + appObj.localize('fr'); + appObj.header = 'french value'; + let serverPayload = appObj.toJSON(); + expect(serverPayload?.localizations?.en?.header).toBe('first value') + expect(serverPayload?.localizations?.fr?.header).toBe('french value') }) \ No newline at end of file From 6b207c28c4f2403ee83db8fab5bf0e5c7b0916a3 Mon Sep 17 00:00:00 2001 From: Juicetan Date: Sat, 31 May 2025 21:44:14 -0400 Subject: [PATCH 3/3] Ensure serialization has symmetric attributes across all locale keys; --- src/models/localizedModel.js | 13 +++++++++++++ tests/object.spec.js | 15 +++++++++++++++ tests/toplevel.spec.js | 14 ++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/models/localizedModel.js b/src/models/localizedModel.js index 73598ac..b28376a 100644 --- a/src/models/localizedModel.js +++ b/src/models/localizedModel.js @@ -159,6 +159,7 @@ class LocalizedModel{ */ inflateLocales(attrKeyMap, arrIndex){ var localeObj = {}; + var localeKeysMap = {}; Object.keys(attrKeyMap).forEach((cacheKey) => { var resolvedCacheKey = cacheKey; @@ -168,6 +169,7 @@ class LocalizedModel{ let attrMap = this._strMap[resolvedCacheKey]; if(attrMap){ Object.keys(attrMap).forEach(function(localeKey){ + localeKeysMap[localeKey] = true; const outputKey = attrKeyMap[cacheKey]; if(!Object.prototype.hasOwnProperty.call(localeObj, localeKey)){ localeObj[localeKey] = {}; @@ -177,6 +179,17 @@ class LocalizedModel{ } }) + Object.keys(localeKeysMap).forEach(function(localeKey){ + let localeMap = localeObj[localeKey] + + Object.keys(attrKeyMap).forEach((cacheKey) => { + const outputKey = attrKeyMap[cacheKey]; + if(!Object.prototype.hasOwnProperty.call(localeMap, outputKey)){ + localeMap[outputKey] = ''; + } + }) + }) + return localeObj; } } diff --git a/tests/object.spec.js b/tests/object.spec.js index c09f72f..834b35c 100644 --- a/tests/object.spec.js +++ b/tests/object.spec.js @@ -91,4 +91,19 @@ test('New instance setting attribute will serialize', () => { expect(serverPayload?.secondLevel).toBeInstanceOf(Object) expect(serverPayload?.secondLevel?.localizations?.en?.header).toBe('first value') expect(serverPayload?.secondLevel?.localizations?.fr?.header).toBe('french value') +}) + +test('Serialization is symmetrical across locales even for empty values', () => { + let appObj = new AppObject(); + appObj.secondLevelObj.header = 'first value'; + appObj.localize('fr'); + appObj.secondLevelObj.header = 'french value'; + let serverPayload = appObj.toJSON(); + expect(serverPayload?.secondLevel).toBeInstanceOf(Object) + expect(serverPayload?.secondLevel?.localizations?.en?.header).toBe('first value') + expect(serverPayload?.secondLevel?.localizations?.en?.subHeader).toBe(''); + expect(serverPayload?.secondLevel?.localizations?.en?.description).toBe(''); + expect(serverPayload?.secondLevel?.localizations?.fr?.header).toBe('french value') + expect(serverPayload?.secondLevel?.localizations?.fr?.subHeader).toBe(''); + expect(serverPayload?.secondLevel?.localizations?.fr?.description).toBe(''); }) \ No newline at end of file diff --git a/tests/toplevel.spec.js b/tests/toplevel.spec.js index e94a1fb..1cb9c89 100644 --- a/tests/toplevel.spec.js +++ b/tests/toplevel.spec.js @@ -88,4 +88,18 @@ test('New instance setting attribute will serialize', () => { let serverPayload = appObj.toJSON(); expect(serverPayload?.localizations?.en?.header).toBe('first value') expect(serverPayload?.localizations?.fr?.header).toBe('french value') +}) + +test('Serialization is symmetrical across locales even for empty values', () => { + let appObj = new AppObject(); + appObj.header = 'first value'; + appObj.localize('fr'); + appObj.header = 'french value'; + let serverPayload = appObj.toJSON(); + expect(serverPayload?.localizations?.en?.header).toBe('first value'); + expect(serverPayload?.localizations?.en?.subHeader).toBe(''); + expect(serverPayload?.localizations?.en?.description).toBe(''); + expect(serverPayload?.localizations?.fr?.header).toBe('french value'); + expect(serverPayload?.localizations?.fr?.subHeader).toBe(''); + expect(serverPayload?.localizations?.fr?.description).toBe(''); }) \ No newline at end of file