From 648e15d88d14f49f23a2a2b41bc4d539d66a4675 Mon Sep 17 00:00:00 2001 From: Adeyemi Adetunji Date: Wed, 24 Apr 2024 23:01:20 +0100 Subject: [PATCH] feat: auto generate translations (#288) * add cloud translation api package * feat: Script that auto generates translations for other languages * add comments * fix: broken translation * improve translation * update locck file * clean up * all round trasnlations check * checkpoint * finish up * move to dev dependencies * fix vercel build error --------- Co-authored-by: Rotimi Best --- apps/dashboard/package.json | 5 +- apps/dashboard/scripts/translate.cjs | 141 ++ .../Exercise/Submissions/Individual.svelte | 4 +- .../Exercise/Submissions/Summary.svelte | 2 +- .../Lesson/Exercise/Submissions/index.svelte | 6 +- .../Lesson/Materials/Video/EmbedLink.svelte | 2 +- .../Materials/Video/YoutubeVideo.svelte | 2 +- .../Org/Settings/OrgSettings.svelte | 2 +- .../Question/TextareaQuestion/index.svelte | 6 +- .../src/lib/utils/translations/de.json | 831 +++++++----- .../src/lib/utils/translations/en.json | 12 +- .../src/lib/utils/translations/es.json | 998 +++++++------- .../src/lib/utils/translations/fr.json | 306 ++--- .../src/lib/utils/translations/hi.json | 266 ++-- .../src/lib/utils/translations/pt.json | 984 +++++++------- .../src/lib/utils/translations/re.json | 1166 ++++++++++++++++ .../src/lib/utils/translations/ru.json | 974 +++++++------ .../src/lib/utils/translations/vi.json | 1202 +++++++++-------- .../routes/lms/community/[slug]/+page.svelte | 6 +- .../org/[slug]/community/[slug]/+page.svelte | 5 +- .../org/[slug]/quiz/[slug]/+page.svelte | 5 +- pnpm-lock.yaml | 114 +- 22 files changed, 4331 insertions(+), 2708 deletions(-) create mode 100644 apps/dashboard/scripts/translate.cjs create mode 100644 apps/dashboard/src/lib/utils/translations/re.json diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7fd8e1fe..fe6c55e9 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -16,7 +16,8 @@ "prepare": "svelte-kit sync", "test": "jest", "test:watch": "pnpm run test --watch", - "ci": "cp .env.example .env && vite build && vite preview" + "ci": "cp .env.example .env && vite build && vite preview", + "script:translate": "node ./scripts/translate.cjs" }, "dependencies": { "@aws-sdk/client-s3": "^3.386.0", @@ -73,12 +74,14 @@ "@testing-library/svelte": "^4.0.5", "@types/jest": "^29.5.10", "@types/lodash": "^4.14.196", + "all-object-keys": "^2.2.0", "autoprefixer": "^10.4.14", "babel-jest": "^29.7.0", "carbon-components-svelte": "^0.79.0", "carbon-icons-svelte": "^12.1.0", "dotenv": "^16.3.1", "eslint-config-custom": "workspace:*", + "jessy": "^3.1.1", "jest": "^29.7.0", "postcss": "^8.4.24", "postcss-load-config": "^4.0.1", diff --git a/apps/dashboard/scripts/translate.cjs b/apps/dashboard/scripts/translate.cjs new file mode 100644 index 00000000..e1da7a54 --- /dev/null +++ b/apps/dashboard/scripts/translate.cjs @@ -0,0 +1,141 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const axios = require('axios'); +const dotenv = require('dotenv'); +const fs = require('fs'); +const path = require('path'); +const keys = require('all-object-keys'); +const jessy = require('jessy'); +const set = require('lodash/set'); + +const englishTranslations = require('../src/lib/utils/translations/en.json'); + +// Load env variables +dotenv.config(); + +const SCRIPT_WAIT_TIME = 2000; + +// Define file paths for each language +const languageFiles = { + hi: path.resolve(__dirname, '../src/lib/utils/translations/hi.json'), + fr: path.resolve(__dirname, '../src/lib/utils/translations/fr.json'), + pt: path.resolve(__dirname, '../src/lib/utils/translations/pt.json'), + de: path.resolve(__dirname, '../src/lib/utils/translations/de.json'), + vi: path.resolve(__dirname, '../src/lib/utils/translations/vi.json'), + ru: path.resolve(__dirname, '../src/lib/utils/translations/re.json'), + es: path.resolve(__dirname, '../src/lib/utils/translations/es.json') +}; + +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const getMissingTranslations = async (toLanguage, outputFilePath) => { + const englishKeys = keys(englishTranslations); + const targetTranslations = require(outputFilePath); // Load target language translations + const targetKeys = keys(targetTranslations); + + const missingKeys = englishKeys.filter((key) => !targetKeys.includes(key)); + + const missingKeysWithDetails = {}; + missingKeys.forEach((key) => { + const value = jessy(key, englishTranslations); + if (value !== undefined) { + missingKeysWithDetails[key] = value; + } + }); + + console.log(`Details of missing keys in ${toLanguage.toUpperCase()}:`); + console.log(missingKeysWithDetails, 'missingKeysWithDetails'); + + // Check if there are missing translations, if not, stop the process + if (!missingKeysWithDetails) { + console.log( + `No missing translations for ${toLanguage.toUpperCase()}. Stopping translation process.` + ); + return; + } + + return missingKeysWithDetails; +}; + +const translateLanguage = async (fromLanguage, toLanguage, outputFilePath) => { + // Check if there are missing translations + const missingTranslations = await getMissingTranslations(toLanguage, outputFilePath); + console.log('Missing translations', missingTranslations); + + if (!Object.keys(missingTranslations).length) { + console.log('No missing translation skip'); + return; + } + + const targetTranslations = require(outputFilePath); // Load target language translations + + const flattenedEnglish = flattenJSON(missingTranslations); + const jsonString = JSON.stringify(flattenedEnglish); + + const encodedParams = new URLSearchParams(); + encodedParams.set('from', fromLanguage); + encodedParams.set('to', toLanguage); + encodedParams.set('json', jsonString); + + const options = { + method: 'POST', + url: 'https://google-translate113.p.rapidapi.com/api/v1/translator/json', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'X-RapidAPI-Key': process.env.RAPID_API_KEY, + 'X-RapidAPI-Host': 'google-translate113.p.rapidapi.com' + }, + data: encodedParams + }; + + try { + console.log('Calling API'); + const response = await axios.request(options); + const { trans } = response.data; + + console.log('Translation gotten', trans); + + if (trans) { + for (const key in trans) { + const value = trans[key]; + set(targetTranslations, key, value); + } + + fs.writeFileSync(outputFilePath, JSON.stringify(targetTranslations, null, 2)); + console.log(`${toLanguage.toUpperCase()} translations updated successfully.`); + } else { + console.log(`Failed to update ${toLanguage.toUpperCase()} translations.`); + } + } catch (error) { + console.error(`Error translating to ${toLanguage.toUpperCase()}:`, error); + } +}; + +// Function to flatten the JSON object +const flattenJSON = (obj, prefix = '') => { + let flattened = {}; + for (const key in obj) { + if (typeof obj[key] === 'object') { + const nested = flattenJSON(obj[key], `${prefix}${key}.`); + flattened = { ...flattened, ...nested }; + } else { + flattened[`${prefix}${key}`] = obj[key]; + } + } + return flattened; +}; + +// Loop through each language and translate the English text with a delay +const translate = async () => { + for (const [language, filePath] of Object.entries(languageFiles)) { + console.log(`============FROM: EN================`); + console.log(`============TO: ${language.toUpperCase()}================`); + + await wait(SCRIPT_WAIT_TIME); + await translateLanguage('en', language, filePath); + + console.log(`============COMPLETE================\n\n`); + } +}; + +translate(); diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte index 8d5f959f..2e789be7 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte @@ -46,7 +46,7 @@ (ans: { question_id: number }) => ans.question_id === q.id ); if (filteredAnswer.some((ans: { open_answer: string }) => ans.open_answer == '')) { - return $t('course.navItem.lessons.exercise.all_exercises.analytics.individual.no'); + return $t('course.navItem.lessons.exercises.all_exercises.analytics.individual.no'); } else { return filteredAnswer.map((ans: { open_answer: any }) => ans.open_answer); } @@ -81,7 +81,7 @@

{$submissions[studentSelected].submitted_by.profile.fullname}'s {$t( - 'course.navItem.lessons.exercise.all_exercises.analytics.individual.answers' + 'course.navItem.lessons.exercises.all_exercises.analytics.individual.answers' )}

{#if $submissions[studentSelected]} diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte index 6a9cf1f7..a867b6d2 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte @@ -144,7 +144,7 @@ {:else if browser}

- {$t('course.navItem.lessons.exercise.all_exercises.analytics.summary.question_chart')} + {$t('course.navItem.lessons.exercises.all_exercises.analytics.summary.question_chart')}

{#each transformedQuestions as q}
diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte index caa4ee3d..5f9bb17c 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte @@ -10,11 +10,11 @@ const tabs = [ { - label: $t('course.navItem.lessons.exercise.all_exercises.analytics.summary.heading'), + label: $t('course.navItem.lessons.exercises.all_exercises.analytics.summary.heading'), value: 'summary' }, { - label: $t('course.navItem.lessons.exercise.all_exercises.analytics.individual.heading'), + label: $t('course.navItem.lessons.exercises.all_exercises.analytics.individual.heading'), value: 'individual' } ]; @@ -47,7 +47,7 @@

{$submissions.length} - {$t('course.navItem.lessons.exercise.all_exercises.analytics.submissions')} + {$t('course.navItem.lessons.exercises.all_exercises.analytics.submissions')}

diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/EmbedLink.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/EmbedLink.svelte index 91fccab7..c1ca4c1a 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/EmbedLink.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/EmbedLink.svelte @@ -19,7 +19,7 @@ const validLinks = links.filter(isValidLink); if (validLinks.length === 0) { - error = 'Invalid link(s)'; + error = $t('course.navItem.lessons.materials.tabs.video.add_video.invalid_link'); } else { const existingLinks = $lesson?.materials?.videos || []; diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/YoutubeVideo.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/YoutubeVideo.svelte index 6c7ca38b..b8c8d11c 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/YoutubeVideo.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/YoutubeVideo.svelte @@ -19,7 +19,7 @@ const validLinks = links.filter(isValidYouTubeLink); if (validLinks.length === 0) { - error = 'Invalid YouTube link(s)'; + error = $t('course.navItem.lessons.materials.tabs.video.add_video.invalid_youtube'); } else { const existingLinks = $lesson?.materials?.videos || []; diff --git a/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte b/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte index df9121bc..66108037 100644 --- a/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte +++ b/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte @@ -218,7 +218,7 @@ {#if $isFreePlan} {/if} - Manage Team + {$t('settings.organization.organization_profile.team.button')} diff --git a/apps/dashboard/src/lib/components/Question/TextareaQuestion/index.svelte b/apps/dashboard/src/lib/components/Question/TextareaQuestion/index.svelte index 6ac3e5f5..3607c2cf 100644 --- a/apps/dashboard/src/lib/components/Question/TextareaQuestion/index.svelte +++ b/apps/dashboard/src/lib/components/Question/TextareaQuestion/index.svelte @@ -61,7 +61,7 @@