Search code examples
javascriptrecursionlocalizationreact-i18next

How can I dynamically replace certain keys in one object with keys from another object?


I have two objects that represent translations for two different languages. They are exactly the same in structure but the values have been translated.

English

{
  about: {
    title: "About",
    subtitle: "Something",
    table: {
      columns: [...],
    }
  },
  products: {
    columns: [...]
  },
  games: {
    details: {
      title: "Game Details",
      columns: [...]
    }
  }
}

French

{
  about: {
    title: "À propos de",
    subtitle: "Quelque chose",
    table: {
      columns: [...],
    }
  },
  products: {
    columns: [...]
  },
  games: {
    details: {
      title: "Détails du jeu",
      columns: [...]
    }
  }
}

I want to retain the French object as is, but replace all instances of columns with the English version from the first object. How can I do that?

The objects I'm using are quite big and deeply nested, so I imagine I need some kind of recursive function. I'm not sure how I can keep track of which key I'm on to do the replacement though.


Solution

  • concrete example

    First we establish a well-defined example, filling in some values for en columns -

    let en = 
      {
        about: {
          title: "About",
          subtitle: "Something",
          table: {
            columns: ["en_about_1", "en_about_2"]   // <-
          }
        },
        products: {
          columns: ["en_products"]                  // <-
        },
        games: {
          details: {
            title: "Game Details",
            columns: ["en_games_1", "en_games_2"]   // <-
          }
        }
      }
    

    And we do the same for fr -

    let fr =
      {
        about: {
          title: "À propos de",
          subtitle: "Quelque chose",
          table: {
            columns: ["fr_apropos_1", "fr_apropos_2"], // <-
          }
        },
        products: {
          columns: ["fr_produit"]                      // <-
        },
        games: {
          details: {
            title: "Détails du jeu",
            columns: ["fr_details_1", "fr_details_2"]  // <-
          }
        }
      }
    

    traverse

    Next we need a way to traverse all paths in a given object -

    function* paths (t)
    { switch(t?.constructor)
      { case Object:
          for (const [k,v] of Object.entries(t))
            for (const path of paths(v))
              yield [k, ...path]
          break
        default:
          yield []
      }
    }
    
    let fr =
      {about: {title: "À propos de",subtitle: "Quelque chose",table: {columns: ["fr_apropos_1", "fr_apropos_2"],}},products: {columns: ["fr_produit"]},games: {details: {title: "Détails du jeu",columns: ["fr_details_1", "fr_details_2"]}}}
      
    for (const path of paths(fr))
      console.log(JSON.stringify(path))

    ["about","title"]
    ["about","subtitle"]
    ["about","table","columns"]
    ["products","columns"]
    ["games","details","title"]
    ["games","details","columns"]
    

    read and write

    Next we'll need a way to read and write values from one object to another -

    1. getAt which takes an object and a path, and returns a value
    2. setAt which takes an object, a path, and a value, and sets a value
    function getAt (t, [k, ...path])
    { if (k == null)
        return t
      else
        return getAt(t?.[k], path)
    }
    
    function setAt (t, [k, ...path], v)
    { if (k == null)
        return v
      else
        return {...t, [k]: setAt(t?.[k] ?? {}, path, v) }
    }
    

    copy at path

    For each path of fr, where the path ends in "columns", update fr at path with the value from en at path -

    for (const path of paths(fr))              // for each path of fr
      if (path.slice(-1)[0] == "columns")      // where the path ends in "columns"
        fr = setAt(fr, path, getAt(en, path))  // update fr at path with value from en at path
        
    console.log(JSON.stringify(fr))
    

    Expand the snippet below and verify the results in your own browser -

    function* paths (t)
    { switch(t?.constructor)
      { case Object:
          for (const [k,v] of Object.entries(t))
            for (const path of paths(v))
              yield [k, ...path]
          break
        default:
          yield []
      }
    }
    
    function getAt (t, [k, ...path])
    { if (k == null)
        return t
      else
        return getAt(t?.[k], path)
    }
    
    function setAt (t, [k, ...path], v)
    { if (k == null)
        return v
      else
        return {...t, [k]: setAt(t?.[k] ?? {}, path, v) }
    }
    
    
    let en =
      {about: {title: "About",subtitle: "Something",table: {columns: ["en_about_1", "en_about_2"]}},products: {columns: ["en_products"]},games: {details: {title: "Game Details",columns: ["en_games_1", "en_games_2"]}}}
    
    let fr =
      {about: {title: "À propos de",subtitle: "Quelque chose",table: {columns: ["fr_apropos_1", "fr_apropos_2"],}},products: {columns: ["fr_produit"]},games: {details: {title: "Détails du jeu",columns: ["fr_details_1", "fr_details_2"]}}}
      
    for (const path of paths(fr))
      if (path.slice(-1)[0] == "columns")
        fr = setAt(fr, path, getAt(en, path))
        
    console.log(JSON.stringify(fr, null, 2))

    {
      "about": {
        "title": "À propos de",
        "subtitle": "Quelque chose",
        "table": {
          "columns": [                      // <-
            "en_about_1",
            "en_about_2" 
          ]
        }
      },
      "products": {
        "columns": [                        // <-
          "en_products"
        ]
      },
      "games": {
        "details": {
          "title": "Détails du jeu",
          "columns": [                      // <-
            "en_games_1",
            "en_games_2"
          ]
        }
      }
    }
    

    All en values are copied to fr for each "columns".