yalla-coop / cost-of-living-support

GNU General Public License v3.0
1 stars 0 forks source link

Translate entire object #255

Open cemalokten opened 1 year ago

cemalokten commented 1 year ago

Issues this PR relates to #225

I've had some time today to work on a different solution for translating JSON objects, with the aim of implementing a more robust solution that resolves the blank spaces bug. Currently we recursively loop through the object and send each string off to be translated individually, which means many hundreds of requests one after the other, occasionally resulting in a blank string being returned and rendered to the page.

I have written a new recursive function (with help from Stackoverflow) that instead of translating individual strings, translates the entire object including the keys. After the translation is complete, the function loops through the new object and replaces the translated keys with the original English keys, for example:

// Pass this to translation
const original = {
  buttons: {
    readMore: "Read more",
    seeAdvice: "See advice",
    goBack: "Go back",
    stuckTalkToSomeOne: "Stuck? Talk to someone",
    accessibility: "Accessibility",
    decreaseTextSize: "- Decrease text size",
    increaseTextSize: "+ Increase text size",
    seeMore: "See more",
    seeLess: "See less",
    addATip: "Add a tip",
    addAnotherTip: "Add another tip",
    addColourOverlay: "Add colour overlay",
  },
  words: { and: "and" },
  placeholders: { select: "Select..." },
  heading: {
    costOfLivingHelper: "Cost of Living Helper",
    shareThisPage: "Share this page",
    helpfulResources: "Helpful resources",
  },
};
// translation output with partially translated keys (unusable by i18next)
const translated = {
  düymələr: {
    Ətraflı: "Ətraflı oxu",
    seeAdvice: "Məsləhətlərə baxın",
    goBack: "Geri qayıt",
    stuckTalkToSomeOne: "İlişmisiniz? Biri ilə danışın",
    əlçatanlıq: "Əlçatanlıq",
    azalmaTextSize: "- Mətn ölçüsünü azaldın",
    artımTextSize: "+ Mətn ölçüsünü artırın",
    SeeMore: "Daha çox bax",
    seeLess: "Daha az görün",
    addATip: "Bir ipucu əlavə et",
    addAnotherTip: "Başqa ipucu əlavə et",
    addColourOverlay: "Rəng örtüyü əlavə et",
  },
  sözlər: { və: "və" },
  yertutucular: { seçin: "Seç..." },
  başlıq: {
    costOfLivingHelper: "Yaşayış Xərcləri Köməkçisi",
    ShareThisPage: "Bu səhifəni paylaş",
    faydalımənbələr: "Faydalı qaynaqlar",
  },
};
// after running the recursive function
{
  buttons: {
    readMore: 'Ətraflı oxu',
    seeAdvice: 'Məsləhətlərə baxın',
    goBack: 'Geri qayıt',
    stuckTalkToSomeOne: 'İlişmisiniz? Biri ilə danışın',
    accessibility: 'Əlçatanlıq',
    decreaseTextSize: '- Mətn ölçüsünü azaldın',
    increaseTextSize: '+ Mətn ölçüsünü artırın',
    seeMore: 'Daha çox bax',
    seeLess: 'Daha az görün',
    addATip: 'Bir ipucu əlavə et',
    addAnotherTip: 'Başqa ipucu əlavə et',
    addColourOverlay: 'Rəng örtüyü əlavə et'
  },
  words: { and: 'və' },
  placeholders: { select: 'Seç...' },
  heading: {
    costOfLivingHelper: 'Yaşayış Xərcləri Köməkçisi',
    shareThisPage: 'Bu səhifəni paylaş',
    helpfulResources: 'Faydalı qaynaqlar'
  }
}

The function looks like this:

const values = (original, translation) => {
  const updated = Object.values(translation);
  return Object.entries(original).map(([key, value], index) => [
    original,
    key,
    value,
    updated[index],
  ]);
};

const replaceValues = (original, translation) => {
  values(original, translation).forEach(
    ([_original, key, origValue, transValue]) => {
      typeof origValue === "object"
        ? replaceValues(origValue, transValue)
        : (_original[key] = transValue);
    },
  );
  return original;
};

replaceValues(structuredClone(original), translation)

The tests compare the original values with the translated values, I've passed in a small amount of the common object and looped the test for all of the languages.

Screenshot 2023-01-26 at 15 12 27

Let me know what you think.

cemalokten commented 1 year ago

I made some mistakes in the testing, I've now found that translating the entire object does mutate the quotation marks around the object values. However, there is an setting for things like brand names, so we can pass in " for all the languages, so when it encounters a " or : it will leave them untranslated. See here : https://docs.aws.amazon.com/translate/latest/dg/creating-custom-terminology.html

@remaininlight Do you think this is possible to implement?

cemalokten commented 1 year ago

I have now realised that the function I wrote yesterday does not work for languages such as Arabic, where characters such as " and , interpreted differently, resulting in invalid JSON. To resolve this I have written a new function that:

1 - Extracts all values from the object into an array 2 - Replaces all characters that are required for JSON with characters that do not get translated when translating to Arabic 3 - Convert array to a string 4 - Translate entire string 5 - Add back in the characters that were removed in step 2 6 - Convert back into an array 7 - Apply the values back onto the object with the correct keys

@RamyAlshurafa If you have a minute to look over this and check the translations are actually correct it would really useful. This feels like a complex route to take, but perhaps it could be improved.

To run the tests with logs, make sure you are in the /server folder and use the command :

jest --silent=false -- translate-entire-object-part2.test.js

Let me know what you think!