From e7c0663ec9a7593a71f16d363fa5350e16f097f7 Mon Sep 17 00:00:00 2001 From: Ramy Shurafa Date: Thu, 26 Jan 2023 14:01:03 +0200 Subject: [PATCH] translate steps partially (#277) * translate steps partially * fix error message * fix throw error syntax --- client/src/App.js | 12 ++-- client/src/api-calls/steps.js | 6 +- client/src/context/steps.js | 10 +++- client/src/pages/Admin/StepForm/index.js | 6 +- client/src/pages/Home/LandingContent.js | 2 +- client/src/pages/Step/index.js | 31 ++++++++-- server/src/database/init/migrations.sql | 3 +- ...add-all-fields-translated-to-steps-i18n.js | 58 +++++++++++++++++++ ...l-fields-translated-to-steps-i18n-down.sql | 6 ++ ...all-fields-translated-to-steps-i18n-up.sql | 9 +++ .../src/database/models/steps-i18n/schema.sql | 1 + .../src/modules/step/controllers/get-step.js | 8 ++- server/src/modules/step/model/find.js | 40 +++++-------- server/src/modules/step/use-cases/get-step.js | 47 ++++++++++----- .../src/modules/step/use-cases/get-steps.js | 22 +++---- .../src/modules/translations/model/create.js | 21 ++++++- .../services/translation/translate-steps.js | 3 +- .../services/translation/translation-api.js | 3 +- 18 files changed, 210 insertions(+), 78 deletions(-) create mode 100644 server/src/database/migrations/20230125051040-add-all-fields-translated-to-steps-i18n.js create mode 100644 server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-down.sql create mode 100644 server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-up.sql diff --git a/client/src/App.js b/client/src/App.js index fe6b1b1b..163608e7 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -43,9 +43,9 @@ function App() { - - - + + + - - - + + + diff --git a/client/src/api-calls/steps.js b/client/src/api-calls/steps.js index 9839ba8f..bf3adb3d 100644 --- a/client/src/api-calls/steps.js +++ b/client/src/api-calls/steps.js @@ -12,9 +12,11 @@ const editStep = async ({ id, form, options } = {}) => { return { error: err }; } }; -const getStepById = async (id, { options } = {}) => { +const getStepById = async ({ id, lng, forPublic, options = {} } = {}) => { try { - const { data } = await axios.get(`${STEPS_BASE}/${id}`); + const { data } = await axios.get(`${STEPS_BASE}/${id}`, { + params: { lng, forPublic }, + }); return { data }; } catch (error) { const err = handleError(error, options); diff --git a/client/src/context/steps.js b/client/src/context/steps.js index a640654f..5bdc089b 100644 --- a/client/src/context/steps.js +++ b/client/src/context/steps.js @@ -1,4 +1,5 @@ import { createContext, useState, useContext, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import { message } from 'antd'; import { useTranslation } from 'react-i18next'; import { useLanguage } from '../helpers'; @@ -86,12 +87,17 @@ const StepsProvider = ({ children, ...props }) => { const [justCompletedId, setJustCompletedId] = useState(''); const [loadingSteps, setLoadingSteps] = useState(false); const [stepsError, setStepsError] = useState(''); + const location = useLocation(); + + const adminPages = location?.pathname?.includes('/admin/'); useEffect(() => { let mounted = true; async function fetchData() { setLoadingSteps(true); - const { data: newSteps, error } = await Steps.getStepsContent({ lng }); + const { data: newSteps, error } = await Steps.getStepsContent({ + lng: adminPages ? 'en' : lng, + }); if (mounted) { let updatedSteps = []; if (error) { @@ -115,7 +121,7 @@ const StepsProvider = ({ children, ...props }) => { return () => { mounted = false; }; - }, [lng]); + }, [lng, adminPages]); useEffect(() => { const updatedSteps = steps.map((step) => { diff --git a/client/src/pages/Admin/StepForm/index.js b/client/src/pages/Admin/StepForm/index.js index 93eb90d2..72d054e2 100644 --- a/client/src/pages/Admin/StepForm/index.js +++ b/client/src/pages/Admin/StepForm/index.js @@ -75,7 +75,11 @@ const StepForm = () => { useEffect(() => { const getStepData = async () => { setState({ loading: true }); - const { error, data } = await Steps.getStepById(stepId); + const { error, data } = await Steps.getStepById({ + id: stepId, + lng: 'en', + forPublic: false, + }); setState({ loading: false }); if (error) { diff --git a/client/src/pages/Home/LandingContent.js b/client/src/pages/Home/LandingContent.js index d872724e..e1424481 100644 --- a/client/src/pages/Home/LandingContent.js +++ b/client/src/pages/Home/LandingContent.js @@ -37,7 +37,7 @@ const LandingContent = ({ uniqueSlug }) => { useEffect(() => { let mounted = true; async function fetchData() { - const hideMessage = message.loading('Loading...'); + const hideMessage = message.loading('Loading...', 0); const { data, error } = await LandingPage.getLandingPageContent({ lng, forPublic: true, diff --git a/client/src/pages/Step/index.js b/client/src/pages/Step/index.js index 3f5ff244..304e82a2 100644 --- a/client/src/pages/Step/index.js +++ b/client/src/pages/Step/index.js @@ -1,8 +1,9 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import B from '../../constants/benefit-calculator'; import { common } from '../../constants'; import { useTranslation } from 'react-i18next'; +import { message } from 'antd'; import { TextWithIcon, @@ -16,8 +17,9 @@ import { } from '../../components'; import { useSteps } from '../../context/steps'; import { navRoutes as n, types } from '../../constants'; - +import * as Steps from '../../api-calls/steps'; import * as S from './style'; +import { useLanguage } from '../../helpers'; import { usePublicOrg } from '../../context/public-org'; @@ -25,14 +27,33 @@ const { Row, Col } = Grid; const { Tips, Checklist } = Cards; const Step = () => { + const [step, setStep] = useState([]); const [stuck, setStuck] = useState(false); const { publicOrg } = usePublicOrg(); const params = useParams(); const navigate = useNavigate(); - const { steps: fullSteps, checkUncheckItem, markAsComplete } = useSteps(); + const { checkUncheckItem, markAsComplete } = useSteps(); const { t } = useTranslation(); + const { lng } = useLanguage(); + + useEffect(() => { + const getSteps = async () => { + const hideMessage = message.loading('Loading...', 0); + + const { data, error } = await Steps.getStepById({ + id: params.id, + lng, + forPublic: true, + }); - const step = fullSteps.find((s) => s.id === Number(params.id)); + hideMessage(); + if (error) { + navigate(n.GENERAL.NOT_FOUND); + } + setStep(data); + }; + getSteps(); + }, [lng, navigate, params.id]); if (!step) { navigate(n.GENERAL.NOT_FOUND); @@ -47,7 +68,7 @@ const Step = () => { }; const checkItem = (itemTitle) => { - const foundItem = step.checklist.find((c) => c.title === itemTitle); + const foundItem = step?.checklist?.find((c) => c.title === itemTitle); return foundItem?.isChecked; }; diff --git a/server/src/database/init/migrations.sql b/server/src/database/init/migrations.sql index 82cf1fdb..b33589eb 100644 --- a/server/src/database/init/migrations.sql +++ b/server/src/database/init/migrations.sql @@ -12,4 +12,5 @@ INSERT INTO "migrations" ("name") VALUES ('/20221124140906-remove-backup-email-unique-constraint'), ('/20221201042815-add-deleted-status-for-organisations'), ('/20221214090033-add-languages-tables'), - ('/20221214173721-convert-json-to-jsonb'); + ('/20221214173721-convert-json-to-jsonb'), + ('/20230125051040-add-all-fields-translated-to-steps-i18n'); diff --git a/server/src/database/migrations/20230125051040-add-all-fields-translated-to-steps-i18n.js b/server/src/database/migrations/20230125051040-add-all-fields-translated-to-steps-i18n.js new file mode 100644 index 00000000..0f8dab9c --- /dev/null +++ b/server/src/database/migrations/20230125051040-add-all-fields-translated-to-steps-i18n.js @@ -0,0 +1,58 @@ +let dbm; +let type; +let seed; +const fs = require('fs'); +const path = require('path'); + +let Promise; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; + Promise = options.Promise; +}; + +exports.up = function (db) { + const filePath = path.join( + __dirname, + 'sqls', + '20230125051040-add-all-fields-translated-to-steps-i18n-up.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) { + if (err) return reject(err); + console.log(`received data: ${data}`); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports.down = function (db) { + const filePath = path.join( + __dirname, + 'sqls', + '20230125051040-add-all-fields-translated-to-steps-i18n-down.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) { + if (err) return reject(err); + console.log(`received data: ${data}`); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports._meta = { + version: 1, +}; diff --git a/server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-down.sql b/server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-down.sql new file mode 100644 index 00000000..4926e4f9 --- /dev/null +++ b/server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE "steps_i18n" + DROP COLUMN "all_fields_translated"; + +COMMIT; \ No newline at end of file diff --git a/server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-up.sql b/server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-up.sql new file mode 100644 index 00000000..d5d23796 --- /dev/null +++ b/server/src/database/migrations/sqls/20230125051040-add-all-fields-translated-to-steps-i18n-up.sql @@ -0,0 +1,9 @@ +BEGIN; + +ALTER TABLE "steps_i18n" + ADD COLUMN "all_fields_translated" BOOLEAN DEFAULT FALSE; + +UPDATE "steps_i18n" + SET "all_fields_translated" = TRUE; + +COMMIT; \ No newline at end of file diff --git a/server/src/database/models/steps-i18n/schema.sql b/server/src/database/models/steps-i18n/schema.sql index b985c81a..db582270 100644 --- a/server/src/database/models/steps-i18n/schema.sql +++ b/server/src/database/models/steps-i18n/schema.sql @@ -14,6 +14,7 @@ CREATE TABLE "steps_i18n" ( "what_you_will_need_to_know" JSONB[], "top_tip" TEXT, "other_tips" TEXT[], + "all_fields_translated" BOOLEAN DEFAULT FALSE, "created_at" TIMESTAMP NOT NULL DEFAULT NOW(), "updated_at" TIMESTAMP NOT NULL DEFAULT NOW() ); diff --git a/server/src/modules/step/controllers/get-step.js b/server/src/modules/step/controllers/get-step.js index c5483aae..c586e735 100644 --- a/server/src/modules/step/controllers/get-step.js +++ b/server/src/modules/step/controllers/get-step.js @@ -3,8 +3,12 @@ import * as steps from '../use-cases'; const getStep = async (req, res, next) => { try { const { id } = req.params; - const { lng } = req.query; - const step = await steps.getStep({ id, lng }); + const { lng, forPublic } = req.query; + const step = await steps.getStep({ + id, + lng, + forPublic: forPublic === 'true', + }); res.json(step); } catch (error) { diff --git a/server/src/modules/step/model/find.js b/server/src/modules/step/model/find.js index 0a7371f9..f6378149 100644 --- a/server/src/modules/step/model/find.js +++ b/server/src/modules/step/model/find.js @@ -8,27 +8,12 @@ const getSteps = async (lng) => { s.step_order, s.title AS "s_en.title", s.description AS "s_en.description", - s.page_title AS "s_en.page_title", - s.page_description AS "s_en.page_description", - s.how_long_does_it_take AS "s_en.how_long_does_it_take", - s.where_do_you_need_to_go AS "s_en.where_do_you_need_to_go", - s.things_you_will_need AS "s_en.things_you_will_need", - s.what_you_will_need_to_know AS "s_en.what_you_will_need_to_know", - s.top_tip AS "s_en.top_tip", - s.other_tips AS "s_en.other_tips", s_i18n.title AS "s_i18n.title", s_i18n.description AS "s_i18n.description", - s_i18n.page_title AS "s_i18n.page_title", - s_i18n.page_description AS "s_i18n.page_description", - s_i18n.how_long_does_it_take AS "s_i18n.how_long_does_it_take", - s_i18n.where_do_you_need_to_go AS "s_i18n.where_do_you_need_to_go", - s_i18n.things_you_will_need AS "s_i18n.things_you_will_need", - s_i18n.what_you_will_need_to_know AS "s_i18n.what_you_will_need_to_know", - s_i18n.top_tip AS "s_i18n.top_tip", - s_i18n.other_tips AS "s_i18n.other_tips", s.is_optional, + s_i18n.all_fields_translated, s_i18n.language_code FROM steps AS s LEFT JOIN steps_i18n AS s_i18n @@ -67,16 +52,19 @@ const getStepById = async (id, lng) => { s.top_tip AS "s_en.top_tip", s.other_tips AS "s_en.other_tips", - s_i18n.title AS "s_i18n.title", - s_i18n.description AS "s_i18n.description", - s_i18n.page_title AS "s_i18n.page_title", - s_i18n.page_description AS "s_i18n.page_description", - s_i18n.how_long_does_it_take AS "s_i18n.how_long_does_it_take", - s_i18n.where_do_you_need_to_go AS "s_i18n.where_do_you_need_to_go", - s_i18n.things_you_will_need AS "s_i18n.things_you_will_need", - s_i18n.what_you_will_need_to_know AS "s_i18n.what_you_will_need_to_know", - s_i18n.top_tip AS "s_i18n.top_tip", - s_i18n.other_tips AS "s_i18n.other_tips", + COALESCE(s_i18n.title, s.title) AS "s_i18n.title", + COALESCE(s_i18n.description, s.description) AS "s_i18n.description", + COALESCE(s_i18n.page_title, s.page_title) AS "s_i18n.page_title", + COALESCE(s_i18n.page_description, s.page_description) AS "s_i18n.page_description", + COALESCE(s_i18n.how_long_does_it_take, s.how_long_does_it_take) AS "s_i18n.how_long_does_it_take", + COALESCE(s_i18n.where_do_you_need_to_go, s.where_do_you_need_to_go) AS "s_i18n.where_do_you_need_to_go", + COALESCE(s_i18n.things_you_will_need, s.things_you_will_need) AS "s_i18n.things_you_will_need", + COALESCE(s_i18n.what_you_will_need_to_know, s.what_you_will_need_to_know) AS "s_i18n.what_you_will_need_to_know", + COALESCE(s_i18n.top_tip, s.top_tip) AS "s_i18n.top_tip", + COALESCE(s_i18n.other_tips, s.other_tips) AS "s_i18n.other_tips", + + s_i18n.language_code, + s_i18n.all_fields_translated, s.is_optional FROM steps AS s LEFT JOIN steps_i18n AS s_i18n diff --git a/server/src/modules/step/use-cases/get-step.js b/server/src/modules/step/use-cases/get-step.js index 8bdd62ff..228bc2ed 100644 --- a/server/src/modules/step/use-cases/get-step.js +++ b/server/src/modules/step/use-cases/get-step.js @@ -1,21 +1,40 @@ import * as Steps from '../model'; -// import translateSteps from '../../../services/translation/translate-steps'; -// import * as Translation from '../../translations/model'; +import translateSteps from '../../../services/translation/translate-steps'; +import * as Translation from '../../translations/model'; -const getStep = async ({ id /* lng */ }) => { - const step = await Steps.getStepById(id, 'en'); - return step; - // return step as is for now, because we don't use this route for public - // const [stepT] = await translateSteps({ - // lng, - // steps: [step], - // }); +const getStep = async ({ id, lng, forPublic }) => { + const step = await Steps.getStepById(id, lng); + if (!forPublic) { + return step; + } - // if (!stepT.isTranslated) { - // Translation.createStepI18n({ stepId: stepT.id, ...stepT }); - // } + const [stepT] = await translateSteps({ + lng, + steps: [step], + }); - // return stepT; + if (!stepT.isTranslated || !step.allFieldsTranslated) { + Translation.createStepI18n({ + stepId: stepT.id, + ...stepT, + allFieldsTranslated: true, + }); + } + + return { + ...stepT, + id: step.id, + checklist: [ + stepT.thingsYouWillNeed.map((item) => ({ + ...item, + stage: 'thingsYouWillNeed', + })), + stepT.whatYouWillNeedToKnow.map((item) => ({ + ...item, + stage: 'whatYouWillNeedToKnow', + })), + ].flat(), + }; }; export default getStep; diff --git a/server/src/modules/step/use-cases/get-steps.js b/server/src/modules/step/use-cases/get-steps.js index 79997faf..73511ece 100644 --- a/server/src/modules/step/use-cases/get-steps.js +++ b/server/src/modules/step/use-cases/get-steps.js @@ -11,24 +11,20 @@ const getSteps = async ({ lng }) => { stepsT.forEach((c) => { if (!c.isTranslated) { - Translation.createStepI18n({ stepId: c.id, ...c }); + Translation.createStepI18n({ + stepId: c.id, + title: c.title, + description: c.description, + languageCode: c.languageCode, + allFieldsTranslated: false, + }); } }); - const stepsTWithChecklist = stepsT.map((step, index) => { + const stepsTWithChecklist = stepsT.map((step) => { return { ...step, - id: index + 1, - checklist: [ - step.thingsYouWillNeed.map((item) => ({ - ...item, - stage: 'thingsYouWillNeed', - })), - step.whatYouWillNeedToKnow.map((item) => ({ - ...item, - stage: 'whatYouWillNeedToKnow', - })), - ].flat(), + id: step.id, }; }); diff --git a/server/src/modules/translations/model/create.js b/server/src/modules/translations/model/create.js index 5456fcc5..2451960d 100644 --- a/server/src/modules/translations/model/create.js +++ b/server/src/modules/translations/model/create.js @@ -72,6 +72,7 @@ const createStepI18n = async ({ whatYouWillNeedToKnow, topTip, otherTips, + allFieldsTranslated, }) => { const sql = ` INSERT INTO steps_i18n ( @@ -86,7 +87,8 @@ const createStepI18n = async ({ things_you_will_need, what_you_will_need_to_know, top_tip, - other_tips + other_tips, + all_fields_translated ) VALUES( $1, $2, @@ -99,9 +101,21 @@ const createStepI18n = async ({ $9, $10::jsonb[], $11, - $12 + $12, + $13 ) - ON CONFLICT (step_id, language_code) DO NOTHING + ON CONFLICT (step_id, language_code) DO UPDATE SET + title = COALESCE($3, steps_i18n.title), + description = COALESCE($4, steps_i18n.description), + page_title = COALESCE($5, steps_i18n.page_title), + page_description = COALESCE($6, steps_i18n.page_description), + how_long_does_it_take = COALESCE($7, steps_i18n.how_long_does_it_take), + where_do_you_need_to_go = COALESCE($8, steps_i18n.where_do_you_need_to_go), + things_you_will_need = COALESCE($9, steps_i18n.things_you_will_need), + what_you_will_need_to_know = COALESCE($10, steps_i18n.what_you_will_need_to_know), + top_tip = COALESCE($11, steps_i18n.top_tip), + other_tips = COALESCE($12, steps_i18n.other_tips), + all_fields_translated = COALESCE($13, steps_i18n.all_fields_translated) RETURNING * `; @@ -118,6 +132,7 @@ const createStepI18n = async ({ whatYouWillNeedToKnow, topTip, otherTips, + allFieldsTranslated, ]; const res = await query(sql, values); diff --git a/server/src/services/translation/translate-steps.js b/server/src/services/translation/translate-steps.js index cb88e8b5..0cf8a7a0 100644 --- a/server/src/services/translation/translate-steps.js +++ b/server/src/services/translation/translate-steps.js @@ -16,9 +16,10 @@ const translateSteps = async ({ lng, steps }) => { topTip, otherTips, id, + allFieldsTranslated, } = step; - if (languageCode === lng || lng === 'en') { + if ((languageCode === lng && allFieldsTranslated) || lng === 'en') { return { ...step, languageCode: lng, diff --git a/server/src/services/translation/translation-api.js b/server/src/services/translation/translation-api.js index dbf11b4a..8305380b 100644 --- a/server/src/services/translation/translation-api.js +++ b/server/src/services/translation/translation-api.js @@ -30,7 +30,8 @@ const translateText = async ({ text, sourceLang, targetLang }) => { const translationData = await translateAWS.translateText(params).promise(); return translationData.TranslatedText; } catch (error) { - throw new Error('translateText API error :>> ', error); + error.extraData = params; + throw error; } };