How to change value of all properties within a nested JSON object?

Viewed 1202

I'm trying to get a key/property from a JSON object and change all of its values, whether its nested within another object, or by itself.

I have a locale variable

const locale = "en" 

and I'm trying to change the value of the returned json object depending on locale, such as

result.navTitle = result.navTitle[locale]

or

stories.map((story)=> story.navTitle = story.navTitle[locale]);
...etc
result = [
{
    "data":{
        "en":"English",
        "fi":"Finnish"
    },
    "navTitle":{
        "en":"English",
        "fi":"Finnish"
    },
    "stories": [
       {
        "navTitle":{"en":"English","fi":"Finnish"},
        "cards":[
            {
                "navTitle":{"en":"English","fi":"Finnish"}
            },
             {
                "navTitle":{"en":"English","fi":"Finnish"}
            }
        ]
       },
       {
        "navTitle":{"en":"English","fi":"Finnish"}
       }
    ]
}
]

I have managed to do this with repetitive .map functions but it gets long, is there any other alternative to do this?

3 Answers

You can use recursion,.

Basically just check if array of object, if array simply map, if it's an object you can use Object.entries / fromEntries to change, inside the loop of the Object.entries you can then test for the lang, if the values of the entries are an object, just pass this to itself for the recursion bit.

Example below..

const result = [{"data":{"en":"English","fi":"Finnish"},"navTitle":{"en":"English","fi":"Finnish"},"stories":[{"navTitle":{"en":"English","fi":"Finnish"},"cards":[{"navTitle":{"en":"English","fi":"Finnish"}},{"navTitle":{"en":"English","fi":"Finnish"}}]},{"navTitle":{"en":"English","fi":"Finnish"}}]}];

function langObj(lang, obj) {
  if (typeof obj === 'object') {
    if (Array.isArray(obj)) {
      return obj.map(m => langObj(lang, m));
    } else {
      return Object.fromEntries(
        Object.entries(obj).map(([k,v]) => {
          const x = v[lang] || v;
          return [k, langObj(lang, x)];
        })
      );
    }
  } else {
    return obj;
  }
}

console.log('English');
console.log(langObj('en', result));
console.log('Finnish');
console.log(langObj('fi', result));

You can create a function, say getLocaleData() to get the relevant data for each locale, adopting a recursive approach to selectively clone the input data.

Once we reach a child object with properties like 'en', 'fi' etc, we'll return only the property that matches the desired locale.

Update: I've added a default value, e.g. '' to return if the locale isn't present and demonstrated with an example. This should now leave non-localized objects as is.

const result = [ { "data":{ "en":"English", "fi":"Finnish" }, features: {}, "navTitle":{ "en":"English", "fi":"Finnish" }, "stories": [ { "navTitle":{"en":"English","fi":"Finnish"}, "cards":[ { "navTitle":{"en":"English","fi":"Finnish"} }, { "navTitle":{"en":"English","fi":"Finnish"} } ] }, { "navTitle":{"en":"English","fi":"Finnish"} } ] } ]

function getLocaleData(obj, locale, defaultValue = '') {
    if (!obj) return obj;
    // If a property key is 'en', 'fi' etc, return the value.
    if (typeof(obj[locale]) === 'string') return obj[locale];
   
    if (Object.keys(obj).length > 0 && Object.keys(obj).every(k => k.length === 2)) { 
        return defaultValue;
    }

    let result = Array.isArray(obj) ? [] : {};
    for (let k in obj) {
        result[k] = (typeof(obj[k]) === "object") ? getLocaleData(obj[k], locale, defaultValue): obj[k];
    }
    return result;
}

console.log('\nEnglish data:', getLocaleData(result, 'en'));
console.log('\nFinnish data:', getLocaleData(result, 'fi'))

// This locale doesn't exist
console.log('\nGerman data:', getLocaleData(result, 'de', ''))
.as-console-wrapper { max-height: 100% !important; top: 0; }

Here is a solution using object-scan that might be a bit easier to reason about.

Note that this solution modifies the result object and does not make a copy. If this is undesired it should be cloned before hand (using eg lodash).

.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/object-scan@18.1.2/lib/index.min.js';

const result = [{ data: { en: 'English', fi: 'Finnish' }, navTitle: { en: 'English', fi: 'Finnish' }, stories: [{ navTitle: { en: 'English', fi: 'Finnish' }, cards: [{ navTitle: { en: 'English', fi: 'Finnish' } }, { navTitle: { en: 'English', fi: 'Finnish' } }] }, { navTitle: { en: 'English', fi: 'Finnish' } }] }];

const rewrite = (data, language) => {
  objectScan([`**.${language}`], {
    filterFn: ({ gparent, gproperty, value }) => {
      if (typeof value === 'string') {
        // eslint-disable-next-line no-param-reassign
        gparent[gproperty] = value;
      }
    }
  })(data);
};

rewrite(result, 'en');

console.log(result);
// => [ { data: 'English', navTitle: 'English', stories: [ { navTitle: 'English', cards: [ { navTitle: 'English' }, { navTitle: 'English' } ] }, { navTitle: 'English' } ] } ]
</script>

Disclaimer: I'm the author of object-scan

Related