From df60907e67aad79dc7a93b429bf1adb1597fbd40 Mon Sep 17 00:00:00 2001 From: frozenhelium Date: Wed, 12 Nov 2025 18:08:12 +0545 Subject: [PATCH 1/3] fix(translatte): update commands --- .../translatte/commands/clearServerStrings.ts | 12 +- .../commands/exportServerStringsToExcel.ts | 4 +- .../translatte/commands/pushMigration.ts | 10 +- .../translatte/commands/pushStringsDref.ts | 153 ++++++++++++++++++ .../commands/pushStringsFromExcel.ts | 94 ++++++++--- app/scripts/translatte/main.ts | 32 ++++ 6 files changed, 273 insertions(+), 32 deletions(-) create mode 100644 app/scripts/translatte/commands/pushStringsDref.ts diff --git a/app/scripts/translatte/commands/clearServerStrings.ts b/app/scripts/translatte/commands/clearServerStrings.ts index 6df5d923b5..d368922403 100644 --- a/app/scripts/translatte/commands/clearServerStrings.ts +++ b/app/scripts/translatte/commands/clearServerStrings.ts @@ -1,11 +1,11 @@ -import { listToGroupList } from "@togglecorp/fujs"; +import { isTruthyString, listToGroupList } from "@togglecorp/fujs"; import { fetchServerState, postLanguageStrings, writeFilePromisify } from "../utils"; async function clearServerStrings(apiUrl: string, authToken: string) { const serverStrings = await fetchServerState(apiUrl, authToken); const bulkActions = listToGroupList( - serverStrings, + serverStrings.filter(({ page_name }) => isTruthyString(page_name)), ({ language }) => language, ({ key, page_name }) => ({ action: "delete" as const, @@ -19,7 +19,7 @@ async function clearServerStrings(apiUrl: string, authToken: string) { response: object, }[] = []; - console.log('Pusing delete actions for en...') + console.log('Pushing delete actions for en...') const enResponse = await postLanguageStrings( 'en', bulkActions.en, @@ -31,7 +31,7 @@ async function clearServerStrings(apiUrl: string, authToken: string) { logs.push({ responseFor: 'en', response: enResponseJson }); - console.log('Pusing delete actions for fr...') + console.log('Pushing delete actions for fr...') const frResponse = await postLanguageStrings( 'fr', bulkActions.fr, @@ -42,7 +42,7 @@ async function clearServerStrings(apiUrl: string, authToken: string) { const frResponseJson = await frResponse.json(); logs.push({ responseFor: 'fr', response: frResponseJson }); - console.log('Pusing delete actions for es...') + console.log('Pushing delete actions for es...') const esResponse = await postLanguageStrings( 'es', bulkActions.es, @@ -52,7 +52,7 @@ async function clearServerStrings(apiUrl: string, authToken: string) { const esResponseJson = await esResponse.json(); logs.push({ responseFor: 'es', response: esResponseJson }); - console.log('Pusing delete actions for ar...') + console.log('Pushing delete actions for ar...') const arResponse = await postLanguageStrings( 'ar', bulkActions.ar, diff --git a/app/scripts/translatte/commands/exportServerStringsToExcel.ts b/app/scripts/translatte/commands/exportServerStringsToExcel.ts index bcfa7f8e93..73b8f966f1 100644 --- a/app/scripts/translatte/commands/exportServerStringsToExcel.ts +++ b/app/scripts/translatte/commands/exportServerStringsToExcel.ts @@ -1,7 +1,7 @@ import xlsx from 'exceljs'; import { fetchServerState } from "../utils"; -import { isFalsyString, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; +import { isFalsyString, isTruthyString, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; async function exportServerStringsToExcel( apiUrl: string, @@ -35,7 +35,7 @@ async function exportServerStringsToExcel( const keyGroupedStrings = mapToList( listToGroupList( - serverStrings, + serverStrings.filter(({ page_name, key }) => isTruthyString(page_name) && isTruthyString(key)), ({ page_name, key }) => `${page_name}:${key}`, ), (list) => { diff --git a/app/scripts/translatte/commands/pushMigration.ts b/app/scripts/translatte/commands/pushMigration.ts index bce31ef3a7..74969d1e9e 100644 --- a/app/scripts/translatte/commands/pushMigration.ts +++ b/app/scripts/translatte/commands/pushMigration.ts @@ -102,6 +102,10 @@ async function pushMigration(migrationFilePath: string, apiUrl: string, authToke const serverActionsForCurrentLanguage = actions.flatMap((actionItem) => { if (language === 'en') { if (actionItem.action === 'add') { + if (isFalsyString(actionItem.value)) { + return undefined; + } + return { action: 'set' as const, key: actionItem.key, @@ -168,7 +172,7 @@ async function pushMigration(migrationFilePath: string, apiUrl: string, authToke ); await writeFilePromisify( - `server-actions.json`, + `/tmp/server-actions.json`, JSON.stringify(serverActions, null, 2), 'utf8', ); @@ -193,7 +197,7 @@ async function pushMigration(migrationFilePath: string, apiUrl: string, authToke const setActions = actions.filter(({ action }) => action === 'set'); const deleteActions = actions.filter(({ action }) => action === 'delete'); - console.log(`Pusing deleted actions for ${lang}...`) + console.log(`Pushing deleted actions for ${lang}...`) const deleteResponse = await postLanguageStrings( lang, deleteActions, @@ -221,7 +225,7 @@ async function pushMigration(migrationFilePath: string, apiUrl: string, authToke await applyAction(serverActions.ar.language, serverActions.ar.actions); await writeFilePromisify( - `push-migration-logs.json`, + `/tmp/push-migration-logs.json`, JSON.stringify(logs, null, 2), 'utf8', ); diff --git a/app/scripts/translatte/commands/pushStringsDref.ts b/app/scripts/translatte/commands/pushStringsDref.ts new file mode 100644 index 0000000000..82d3e5c7d3 --- /dev/null +++ b/app/scripts/translatte/commands/pushStringsDref.ts @@ -0,0 +1,153 @@ +import xlsx, { CellValue } from 'exceljs'; +import { fetchServerState, postLanguageStrings } from "../utils"; +import { encodeDate, isDefined, isNotDefined, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; +import { Language, ServerActionItem, SourceStringItem } from '../types'; + + +function getValueFromCellValue(cellValue: CellValue) { + if (isNotDefined(cellValue)) { + return undefined; + } + + if ( + typeof cellValue === 'number' + || typeof cellValue === 'string' + || typeof cellValue === 'boolean' + ) { + return cellValue; + } + + if (cellValue instanceof Date) { + return encodeDate(cellValue); + } + + if ('error' in cellValue) { + return undefined; + } + + if ('richText' in cellValue) { + return cellValue.richText.map(({ text }) => text).join(''); + } + + if ('hyperlink' in cellValue) { + const MAIL_IDENTIFIER = 'mailto:'; + if (cellValue.hyperlink.startsWith(MAIL_IDENTIFIER)) { + return cellValue.hyperlink.substring(MAIL_IDENTIFIER.length); + } + + return cellValue.hyperlink; + } + + if (isNotDefined(cellValue.result)) { + return undefined; + } + + if (typeof cellValue.result === 'object' && 'error' in cellValue.result) { + return undefined; + } + + // Formula result + return getValueFromCellValue(cellValue.result); +} + +async function pushStringsDref(importFilePath: string, apiUrl: string, accessToken: string) { + const strings = await fetchServerState(apiUrl); + const enStrings = strings.filter((string) => string.language === 'en'); + + const workbook = new xlsx.Workbook(); + + await workbook.xlsx.readFile(importFilePath); + + const firstSheet = workbook.worksheets[0]; + const columns = firstSheet.columns.map( + (column) => { + const key = column.values?.[1]?.toString(); + if (isNotDefined(key)) { + return undefined; + } + return { key, column: column.number } + } + ).filter(isDefined); + + const columnMap = listToMap( + columns, + ({ key }) => key, + ({ column }) => column, + ); + + const updatedStrings: SourceStringItem[] = []; + + firstSheet.eachRow((row) => { + const enColumnKey = columnMap['EN']; + const frColumnKey = columnMap['FR']; + const esColumnKey = columnMap['ES']; + const arColumnKey = columnMap['AR']; + + const enValue = isDefined(enColumnKey) ? getValueFromCellValue(row.getCell(enColumnKey).value) : undefined; + + const string = enStrings.find(({ value }) => value === enValue); + + if (string) { + const frValue = isDefined(frColumnKey) ? getValueFromCellValue(row.getCell(frColumnKey).value) : undefined; + const esValue = isDefined(esColumnKey) ? getValueFromCellValue(row.getCell(esColumnKey).value) : undefined; + const arValue = isDefined(arColumnKey) ? getValueFromCellValue(row.getCell(arColumnKey).value) : undefined; + + updatedStrings.push({ + ...string, + language: 'fr', + value: String(frValue), + }); + + updatedStrings.push({ + ...string, + language: 'es', + value: String(esValue), + }); + + updatedStrings.push({ + ...string, + language: 'ar', + value: String(arValue), + }); + } + }); + + const languageGroupedActions = mapToList( + listToGroupList( + updatedStrings, + ({ language }) => language, + (languageString) => { + const serverAction: ServerActionItem = { + action: 'set', + key: languageString.key, + page_name: languageString.page_name, + value: languageString.value, + hash: languageString.hash, + } + + return serverAction; + }, + ), + (actions, language) => ({ + language: language as Language, + actions, + }) + ); + + for (let i = 0; i < languageGroupedActions.length; i++) { + const action = languageGroupedActions[i]; + + console.log(`posting ${action.language} actions...`); + const result = await postLanguageStrings( + action.language, + action.actions, + apiUrl, + accessToken, + ) + + const resultJson = await result.json(); + console.info(resultJson); + } +} + +export default pushStringsDref; diff --git a/app/scripts/translatte/commands/pushStringsFromExcel.ts b/app/scripts/translatte/commands/pushStringsFromExcel.ts index b9da7b38b5..0cf234a60e 100644 --- a/app/scripts/translatte/commands/pushStringsFromExcel.ts +++ b/app/scripts/translatte/commands/pushStringsFromExcel.ts @@ -1,10 +1,56 @@ -import xlsx from 'exceljs'; +import xlsx, { CellValue } from 'exceljs'; import { Md5 } from 'ts-md5'; -import { isDefined, isNotDefined, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; +import { encodeDate, isDefined, isNotDefined, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; import { Language, ServerActionItem } from '../types'; import { postLanguageStrings } from '../utils'; +function getValueFromCellValue(cellValue: CellValue) { + if (isNotDefined(cellValue)) { + return undefined; + } + + if ( + typeof cellValue === 'number' + || typeof cellValue === 'string' + || typeof cellValue === 'boolean' + ) { + return cellValue; + } + + if (cellValue instanceof Date) { + return encodeDate(cellValue); + } + + if ('error' in cellValue) { + return undefined; + } + + if ('richText' in cellValue) { + return cellValue.richText.map(({ text }) => text).join(''); + } + + if ('hyperlink' in cellValue) { + const MAIL_IDENTIFIER = 'mailto:'; + if (cellValue.hyperlink.startsWith(MAIL_IDENTIFIER)) { + return cellValue.hyperlink.substring(MAIL_IDENTIFIER.length); + } + + return cellValue.hyperlink; + } + + if (isNotDefined(cellValue.result)) { + return undefined; + } + + if (typeof cellValue.result === 'object' && 'error' in cellValue.result) { + return undefined; + } + + // Formula result + return getValueFromCellValue(cellValue.result); +} + async function pushStringsFromExcel(importFilePath: string, apiUrl: string, accessToken: string) { const workbook = new xlsx.Workbook(); @@ -37,27 +83,27 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce firstSheet.eachRow( (row) => { - const keyColumn = columnMap['key']; - const key = isDefined(keyColumn) ? row.getCell(keyColumn).value?.toString() : undefined; + const keyColumn = columnMap['Key']; + const key = isDefined(keyColumn) ? String(getValueFromCellValue(row.getCell(keyColumn).value)) : undefined; - const namespaceColumn = columnMap['namespace']; - const namespace = isDefined(namespaceColumn) ? row.getCell(namespaceColumn).value?.toString() : undefined; + const namespaceColumn = columnMap['Namespace']; + const namespace = isDefined(namespaceColumn) ? String(getValueFromCellValue(row.getCell(namespaceColumn).value)) : undefined; if (isNotDefined(key) || isNotDefined(namespace)) { return; } - const enColumn = columnMap['en']; - const en = isDefined(enColumn) ? row.getCell(enColumn).value?.toString() : undefined; + const enColumn = columnMap['EN']; + const en = isDefined(enColumn) ? String(getValueFromCellValue(row.getCell(enColumn).value)) : undefined; - const arColumn = columnMap['ar']; - const ar = isDefined(arColumn) ? row.getCell(arColumn).value?.toString() : undefined; + const arColumn = columnMap['AR']; + const ar = isDefined(arColumn) ? String(getValueFromCellValue(row.getCell(arColumn).value)) : undefined; - const frColumn = columnMap['fr']; - const fr = isDefined(frColumn) ? row.getCell(frColumn).value?.toString() : undefined; + const frColumn = columnMap['FR']; + const fr = isDefined(frColumn) ? String(getValueFromCellValue(row.getCell(frColumn).value)) : undefined; - const esColumn = columnMap['es']; - const es = isDefined(esColumn) ? row.getCell(esColumn).value?.toString() : undefined; + const esColumn = columnMap['ES']; + const es = isDefined(esColumn) ? String(getValueFromCellValue(row.getCell(esColumn).value)) : undefined; if (isNotDefined(en)) { return; @@ -65,6 +111,7 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce const hash = Md5.hashStr(en); + /* strings.push({ key, namespace, @@ -72,6 +119,7 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce value: en, hash, }); + */ if (isDefined(ar)) { strings.push({ @@ -127,16 +175,20 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce }) ); - const postPromises = languageGroupedActions.map( - (languageStrings) => postLanguageStrings( - languageStrings.language, - languageStrings.actions, + for (let i = 0; i < languageGroupedActions.length; i++) { + const action = languageGroupedActions[i]; + + console.log(`posting ${action.language} actions...`); + const result = await postLanguageStrings( + action.language, + action.actions, apiUrl, accessToken, - ) - ) + ); - await Promise.all(postPromises); + const resultJson = await result.text(); + console.info(resultJson); + } } export default pushStringsFromExcel; diff --git a/app/scripts/translatte/main.ts b/app/scripts/translatte/main.ts index f8dd65a23f..905c6a04b9 100644 --- a/app/scripts/translatte/main.ts +++ b/app/scripts/translatte/main.ts @@ -14,6 +14,7 @@ import pushMigration from './commands/pushMigration'; import pushStringsFromExcel from './commands/pushStringsFromExcel'; import exportServerStringsToExcel from './commands/exportServerStringsToExcel'; import clearServerStrings from './commands/clearServerStrings'; +import pushStringsDref from './commands/pushStringsDref'; const currentDir = cwd(); @@ -254,6 +255,37 @@ yargs(hideBin(process.argv)) ); }, ) + .command( + 'push-strings-dref ', + 'IMPORTANT!!! Temporary command, do not use!', + (yargs) => { + yargs.positional('IMPORT_FILE_PATH', { + type: 'string', + describe: 'Find the import file on IMPORT_FILE_PATH', + }); + yargs.options({ + 'auth-token': { + type: 'string', + describe: 'Authentication token to access the API server', + require: true, + }, + 'api-url': { + type: 'string', + describe: 'URL for the API server', + require: true, + } + }); + }, + async (argv) => { + const importFilePath = (argv.IMPORT_FILE_PATH as string); + + await pushStringsDref( + importFilePath, + argv.apiUrl as string, + argv.authToken as string, + ); + }, + ) .command( 'export-server-strings ', 'Export server strings to excel file', From 1ce897c60868551bbe9e1937fc0bfeb5c17669e1 Mon Sep 17 00:00:00 2001 From: frozenhelium Date: Thu, 20 Nov 2025 16:30:34 +0545 Subject: [PATCH 2/3] feat(translatte): add command to sync en strings --- .../translatte/commands/pushStringsDref.ts | 63 +++++++++++++++++-- .../commands/pushStringsFromExcel.ts | 30 ++++++--- .../translatte/commands/syncEnStrings.ts | 39 ++++++++++++ app/scripts/translatte/main.ts | 31 +++++++++ go-api | 2 +- 5 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 app/scripts/translatte/commands/syncEnStrings.ts diff --git a/app/scripts/translatte/commands/pushStringsDref.ts b/app/scripts/translatte/commands/pushStringsDref.ts index 82d3e5c7d3..062ca3820e 100644 --- a/app/scripts/translatte/commands/pushStringsDref.ts +++ b/app/scripts/translatte/commands/pushStringsDref.ts @@ -2,6 +2,7 @@ import xlsx, { CellValue } from 'exceljs'; import { fetchServerState, postLanguageStrings } from "../utils"; import { encodeDate, isDefined, isNotDefined, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; import { Language, ServerActionItem, SourceStringItem } from '../types'; +import { Md5 } from 'ts-md5'; function getValueFromCellValue(cellValue: CellValue) { @@ -78,6 +79,16 @@ async function pushStringsDref(importFilePath: string, apiUrl: string, accessTok const updatedStrings: SourceStringItem[] = []; firstSheet.eachRow((row) => { + const keyColumn = columnMap['Key']; + const key = isDefined(keyColumn) ? String(getValueFromCellValue(row.getCell(keyColumn).value)) : undefined; + + const namespaceColumn = columnMap['Namespace']; + const namespace = isDefined(namespaceColumn) ? String(getValueFromCellValue(row.getCell(namespaceColumn).value)) : undefined; + + if (isNotDefined(key) || isNotDefined(namespace)) { + return; + } + const enColumnKey = columnMap['EN']; const frColumnKey = columnMap['FR']; const esColumnKey = columnMap['ES']; @@ -85,27 +96,69 @@ async function pushStringsDref(importFilePath: string, apiUrl: string, accessTok const enValue = isDefined(enColumnKey) ? getValueFromCellValue(row.getCell(enColumnKey).value) : undefined; - const string = enStrings.find(({ value }) => value === enValue); + const strings = enStrings.filter(({ value }) => value === enValue); + + if (strings.length > 0) { + strings.forEach((string) => { + const frValue = isDefined(frColumnKey) ? getValueFromCellValue(row.getCell(frColumnKey).value) : undefined; + const esValue = isDefined(esColumnKey) ? getValueFromCellValue(row.getCell(esColumnKey).value) : undefined; + const arValue = isDefined(arColumnKey) ? getValueFromCellValue(row.getCell(arColumnKey).value) : undefined; + + updatedStrings.push({ + ...string, + language: 'fr', + value: String(frValue), + }); + + updatedStrings.push({ + ...string, + language: 'es', + value: String(esValue), + }); + + updatedStrings.push({ + ...string, + language: 'ar', + value: String(arValue), + }); + }); + } - if (string) { + if (strings.length === 0) { const frValue = isDefined(frColumnKey) ? getValueFromCellValue(row.getCell(frColumnKey).value) : undefined; const esValue = isDefined(esColumnKey) ? getValueFromCellValue(row.getCell(esColumnKey).value) : undefined; const arValue = isDefined(arColumnKey) ? getValueFromCellValue(row.getCell(arColumnKey).value) : undefined; + const hash = Md5.hashStr(String(enValue)); + + updatedStrings.push({ + key, + page_name: namespace, + hash, + language: 'en', + value: String(enValue), + }); + updatedStrings.push({ - ...string, + key, + page_name: namespace, + hash, language: 'fr', value: String(frValue), }); updatedStrings.push({ - ...string, + key, + page_name: namespace, + hash, language: 'es', value: String(esValue), }); updatedStrings.push({ - ...string, + key, + page_name: namespace, + hash, language: 'ar', value: String(arValue), }); diff --git a/app/scripts/translatte/commands/pushStringsFromExcel.ts b/app/scripts/translatte/commands/pushStringsFromExcel.ts index 0cf234a60e..b0ed5ccab5 100644 --- a/app/scripts/translatte/commands/pushStringsFromExcel.ts +++ b/app/scripts/translatte/commands/pushStringsFromExcel.ts @@ -1,9 +1,9 @@ import xlsx, { CellValue } from 'exceljs'; import { Md5 } from 'ts-md5'; -import { encodeDate, isDefined, isNotDefined, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; +import { encodeDate, isDefined, isFalsyString, isNotDefined, isTruthyString, listToGroupList, listToMap, mapToList } from '@togglecorp/fujs'; import { Language, ServerActionItem } from '../types'; -import { postLanguageStrings } from '../utils'; +import { postLanguageStrings, writeFilePromisify } from '../utils'; function getValueFromCellValue(cellValue: CellValue) { if (isNotDefined(cellValue)) { @@ -82,20 +82,28 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce }[] = []; firstSheet.eachRow( - (row) => { + (row, i) => { + if (i === 0) { + return; + } + const keyColumn = columnMap['Key']; const key = isDefined(keyColumn) ? String(getValueFromCellValue(row.getCell(keyColumn).value)) : undefined; const namespaceColumn = columnMap['Namespace']; const namespace = isDefined(namespaceColumn) ? String(getValueFromCellValue(row.getCell(namespaceColumn).value)) : undefined; - if (isNotDefined(key) || isNotDefined(namespace)) { + if (isFalsyString(key) || isFalsyString(namespace)) { return; } const enColumn = columnMap['EN']; const en = isDefined(enColumn) ? String(getValueFromCellValue(row.getCell(enColumn).value)) : undefined; + if (isFalsyString(en)) { + return; + } + const arColumn = columnMap['AR']; const ar = isDefined(arColumn) ? String(getValueFromCellValue(row.getCell(arColumn).value)) : undefined; @@ -111,7 +119,6 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce const hash = Md5.hashStr(en); - /* strings.push({ key, namespace, @@ -119,9 +126,8 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce value: en, hash, }); - */ - if (isDefined(ar)) { + if (isTruthyString(ar)) { strings.push({ key, namespace, @@ -131,7 +137,7 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce }); } - if (isDefined(fr)) { + if (isTruthyString(fr)) { strings.push({ key, namespace, @@ -141,7 +147,7 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce }); } - if (isDefined(es)) { + if (isTruthyString(es)) { strings.push({ key, namespace, @@ -175,6 +181,12 @@ async function pushStringsFromExcel(importFilePath: string, apiUrl: string, acce }) ); + await writeFilePromisify( + '/tmp/language-grouped-actions.json', + JSON.stringify(languageGroupedActions, null, 2), + 'utf8', + ); + for (let i = 0; i < languageGroupedActions.length; i++) { const action = languageGroupedActions[i]; diff --git a/app/scripts/translatte/commands/syncEnStrings.ts b/app/scripts/translatte/commands/syncEnStrings.ts new file mode 100644 index 0000000000..3b3b7982ef --- /dev/null +++ b/app/scripts/translatte/commands/syncEnStrings.ts @@ -0,0 +1,39 @@ +import { isDefined, isFalsyString } from "@togglecorp/fujs"; +import { fetchServerState, postLanguageStrings, writeFilePromisify } from "../utils"; + +async function syncEnStrings(sourceApiUrl: string, desinationApiUrl: string, authToken: string) { + const serverStrings = await fetchServerState(sourceApiUrl, authToken); + const enStrings = serverStrings.filter((string) => string.language === 'en'); + + const actions = enStrings.map((string) => { + if (isFalsyString(string.key) || isFalsyString(string.page_name) || isFalsyString(string.value)) { + return undefined; + } + + return { + action: 'set' as const, + key: string.key, + page_name: string.page_name, + value: string.value, + hash: string.hash, + }; + }).filter(isDefined); + + console.log("posting en actions..."); + const result = await postLanguageStrings( + 'en', + actions, + desinationApiUrl, + authToken, + ) + + const resultJson = await result.json(); + console.info(resultJson); + await writeFilePromisify( + '/tmp/sync-en-strings-logs.json', + JSON.stringify(resultJson, null, 2), + 'utf8', + ); +} + +export default syncEnStrings; diff --git a/app/scripts/translatte/main.ts b/app/scripts/translatte/main.ts index 905c6a04b9..9a2f34c02f 100644 --- a/app/scripts/translatte/main.ts +++ b/app/scripts/translatte/main.ts @@ -15,6 +15,7 @@ import pushStringsFromExcel from './commands/pushStringsFromExcel'; import exportServerStringsToExcel from './commands/exportServerStringsToExcel'; import clearServerStrings from './commands/clearServerStrings'; import pushStringsDref from './commands/pushStringsDref'; +import syncEnStrings from './commands/syncEnStrings'; const currentDir = cwd(); @@ -286,6 +287,36 @@ yargs(hideBin(process.argv)) ); }, ) + .command( + 'sync-en-strings', + 'IMPORTANT!!! Temporary command, do not use!', + (yargs) => { + yargs.options({ + 'auth-token': { + type: 'string', + describe: 'Authentication token to access the API server', + require: true, + }, + 'source-api-url': { + type: 'string', + describe: 'URL for the source API server', + require: true, + }, + 'destination-api-url': { + type: 'string', + describe: 'URL for the destination', + require: true, + } + }); + }, + async (argv) => { + await syncEnStrings( + argv.sourceApiUrl as string, + argv.destinationApiUrl as string, + argv.authToken as string, + ); + }, + ) .command( 'export-server-strings ', 'Export server strings to excel file', diff --git a/go-api b/go-api index 596b1f2c24..14be734d91 160000 --- a/go-api +++ b/go-api @@ -1 +1 @@ -Subproject commit 596b1f2c24014148879a97cc069a62205dfdc24d +Subproject commit 14be734d91ca8e26e2727a9814935a85750d51ec From 9a45b168adb691328b4d865c7642cd48bbc61ee2 Mon Sep 17 00:00:00 2001 From: frozenhelium Date: Thu, 9 Oct 2025 14:08:22 +0545 Subject: [PATCH 3/3] feat(translation): connect with ifrc translation service --- .github/workflows/ci.yml | 2 + app/env.ts | 4 + app/package.json | 5 +- app/scripts/fix-generated.ts | 9 + .../commands/pushStringsFromExcelToIfrc.ts | 103 +++ .../translatte/commands/syncEnStrings.ts | 4 +- app/scripts/translatte/main.ts | 34 +- app/src/config.ts | 4 + app/src/utils/resolveUrl.ts | 12 +- app/src/utils/restRequest/go.ts | 44 +- app/src/utils/restRequest/index.ts | 32 +- app/src/views/RootLayout/index.tsx | 133 ++-- app/translationTypes.ts | 586 ++++++++++++++++++ app/tsconfig.json | 3 +- nginx-serve/Dockerfile | 2 + nginx-serve/apply-config.sh | 2 + nginx-serve/helm/templates/configmap.yaml | 2 + nginx-serve/helm/values-test.yaml | 2 + nginx-serve/helm/values.yaml | 2 + 19 files changed, 883 insertions(+), 102 deletions(-) create mode 100644 app/scripts/fix-generated.ts create mode 100644 app/scripts/translatte/commands/pushStringsFromExcelToIfrc.ts create mode 100644 app/translationTypes.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19e9c4d306..66f327b67b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,13 @@ env: APP_MAPBOX_ACCESS_TOKEN: ${{ vars.APP_MAPBOX_ACCESS_TOKEN }} APP_RISK_ADMIN_URL: ${{ vars.APP_RISK_ADMIN_URL }} APP_RISK_API_ENDPOINT: ${{ vars.APP_RISK_API_ENDPOINT }} + APP_TRANSLATION_API_ENDPOINT: ${{ vars.APP_TRANSLATION_API_ENDPOINT }} APP_SENTRY_DSN: ${{ vars.APP_SENTRY_DSN }} APP_SENTRY_NORMALIZE_DEPTH: ${{ vars.APP_SENTRY_NORMALIZE_DEPTH }} APP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.APP_SENTRY_TRACES_SAMPLE_RATE }} APP_SHOW_ENV_BANNER: ${{ vars.APP_SHOW_ENV_BANNER }} APP_TINY_API_KEY: ${{ vars.APP_TINY_API_KEY }} + APP_TRANSLATION_API_KEY: ${{ vars.APP_TRANSLATION_API_KEY }} APP_TITLE: ${{ vars.APP_TITLE }} GITHUB_WORKFLOW: true diff --git a/app/env.ts b/app/env.ts index d0e7ea635c..8e3b64f5e4 100644 --- a/app/env.ts +++ b/app/env.ts @@ -16,6 +16,10 @@ export default defineConfig({ return value as ('production' | 'staging' | 'testing' | `alpha-${number}` | 'development' | 'APP_ENVIRONMENT_PLACEHOLDER'); }, APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }), + + APP_TRANSLATION_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }), + APP_TRANSLATION_API_KEY: Schema.string(), + APP_ADMIN_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }), APP_MAPBOX_ACCESS_TOKEN: Schema.string(), APP_TINY_API_KEY: Schema.string(), diff --git a/app/package.json b/app/package.json index 25f33196fa..f3a6cfb38a 100644 --- a/app/package.json +++ b/app/package.json @@ -13,12 +13,15 @@ "translatte": "tsx scripts/translatte/main.ts", "translatte:generate": "pnpm translatte generate-migration ../translationMigrations ./src/**/i18n.json ../packages/ui/src/**/i18n.json", "translatte:lint": "pnpm translatte lint ./src/**/i18n.json ../packages/ui/src/**/i18n.json", - "initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api", + "initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api && pnpm initialize:type:translations", "initialize:type:go-api": "test -f ./generated/types.ts && true || cp types.stub.ts ./generated/types.ts", "initialize:type:risk-api": "test -f ./generated/riskTypes.ts && true || cp types.stub.ts ./generated/riskTypes.ts", + "initialize:type:translations": "test -f ./generated/translationTypes.ts && true || cp types.stub.ts ./generated/translationTypes.ts", "generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api", "generate:type:go-api": "GO_API_HASH=$(git rev-parse HEAD:go-api); dotenv -- cross-var openapi-typescript https://raw.githubusercontent.com/IFRCGo/go-api-artifacts/refs/heads/main/generated/$GO_API_HASH/openapi-schema.yaml -o ./generated/types.ts --alphabetize", "generate:type:risk-api": "dotenv -- cross-var openapi-typescript ../go-risk-module-api/openapi-schema.yaml -o ./generated/riskTypes.ts --alphabetize", + "generate:type:translations": "dotenv -- cross-var openapi-typescript \"%APP_TRANSLATION_API_ENDPOINT%swagger/v1/swagger.json/\" -o ./generated/translationTypes.ts --alphabetize", + "postgenerate:type:translations": "tsx scripts/fix-generated.ts", "prestart": "pnpm initialize:type", "start": "pnpm -F @ifrc-go/ui build && vite", "prebuild": "pnpm initialize:type", diff --git a/app/scripts/fix-generated.ts b/app/scripts/fix-generated.ts new file mode 100644 index 0000000000..90a4c61722 --- /dev/null +++ b/app/scripts/fix-generated.ts @@ -0,0 +1,9 @@ +import { readFileSync, writeFileSync } from 'fs'; + +const path = 'generated/translationTypes.ts'; + +const content = readFileSync(path, 'utf-8'); + +// If already added, skip +writeFileSync(path, `// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-nocheck\n${content}`); +console.log('✔ Added // @ts-nocheck to translationTypes.ts'); diff --git a/app/scripts/translatte/commands/pushStringsFromExcelToIfrc.ts b/app/scripts/translatte/commands/pushStringsFromExcelToIfrc.ts new file mode 100644 index 0000000000..e3b20ae103 --- /dev/null +++ b/app/scripts/translatte/commands/pushStringsFromExcelToIfrc.ts @@ -0,0 +1,103 @@ +import { readFileSync } from "fs"; + +// FIXME: get this from params +const applicationId = 18; + +function resolveUrl(from: string, to: string) { + const resolvedUrl = new URL(to, new URL(from, 'resolve://')); + if (resolvedUrl.protocol === 'resolve:') { + const { pathname, search, hash } = resolvedUrl; + return pathname + search + hash; + } + return resolvedUrl.toString(); +} + +/* +async function fetchTranslations(ifrcApiUrl: string, ifrcApiKey: string) { + const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation/`); + + const headers: RequestInit['headers'] = { + 'Accept': 'application/json', + 'X-API-KEY': ifrcApiKey, + } + + const promise = fetch( + endpoint, + { + method: 'GET', + headers, + } + ); + + return promise; +} + +async function postTranslation(ifrcApiUrl: string, ifrcApiKey: string) { + const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation`); + + const headers: RequestInit['headers'] = { + // 'Accept': 'application/json', + 'X-API-KEY': ifrcApiKey, + 'Content-Type': 'application/json', + } + + const promise = fetch( + endpoint, + { + method: 'POST', + headers, + body: JSON.stringify({ + page: 'home', + keyName: 'pageTitle', + value: 'IFRC GO | Home', + languageCode: 'en', + }), + } + ); + + return promise; +} +*/ + +async function fullAppImport(importFilePath: string, ifrcApiUrl: string, ifrcApiKey: string) { + const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation/fullappimport`); + const translationFile = readFileSync(importFilePath); + const uint8FileData = new Uint8Array(translationFile); + const blob = new Blob([uint8FileData], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + + const formData = new FormData(); + formData.append('files', blob, 'translations.xlsx'); + + const headers: RequestInit['headers'] = { + 'Accept': 'application/json', + 'X-API-KEY': ifrcApiKey, + } + + const promise = fetch( + endpoint, + { + method: 'POST', + headers, + body: formData, + } + ); + + return promise; +} + +async function pushStringsFromExcelToIfrc(importFilePath: string, apiUrl: string, apiKey: string) { + const response = await fullAppImport(importFilePath, apiUrl, apiKey); + + try { + const responseJson = await response.json(); + console.info(responseJson); + } catch(e) { + console.info(e); + const responseText = await response.text(); + console.info(responseText); + } +} + +export default pushStringsFromExcelToIfrc; diff --git a/app/scripts/translatte/commands/syncEnStrings.ts b/app/scripts/translatte/commands/syncEnStrings.ts index 3b3b7982ef..023abae742 100644 --- a/app/scripts/translatte/commands/syncEnStrings.ts +++ b/app/scripts/translatte/commands/syncEnStrings.ts @@ -1,7 +1,7 @@ import { isDefined, isFalsyString } from "@togglecorp/fujs"; import { fetchServerState, postLanguageStrings, writeFilePromisify } from "../utils"; -async function syncEnStrings(sourceApiUrl: string, desinationApiUrl: string, authToken: string) { +async function syncEnStrings(sourceApiUrl: string, destinationApiUrl: string, authToken: string) { const serverStrings = await fetchServerState(sourceApiUrl, authToken); const enStrings = serverStrings.filter((string) => string.language === 'en'); @@ -23,7 +23,7 @@ async function syncEnStrings(sourceApiUrl: string, desinationApiUrl: string, aut const result = await postLanguageStrings( 'en', actions, - desinationApiUrl, + destinationApiUrl, authToken, ) diff --git a/app/scripts/translatte/main.ts b/app/scripts/translatte/main.ts index 9a2f34c02f..74152ba5c1 100644 --- a/app/scripts/translatte/main.ts +++ b/app/scripts/translatte/main.ts @@ -16,6 +16,7 @@ import exportServerStringsToExcel from './commands/exportServerStringsToExcel'; import clearServerStrings from './commands/clearServerStrings'; import pushStringsDref from './commands/pushStringsDref'; import syncEnStrings from './commands/syncEnStrings'; +import pushStringsFromExcelToIfrc from './commands/pushStringsFromExcelToIfrc'; const currentDir = cwd(); @@ -256,6 +257,37 @@ yargs(hideBin(process.argv)) ); }, ) + .command( + 'push-strings-from-excel-to-ifrc ', + 'Import migration from excel file and push it to server', + (yargs) => { + yargs.positional('IMPORT_FILE_PATH', { + type: 'string', + describe: 'Find the import file on IMPORT_FILE_PATH', + }); + yargs.options({ + 'api-key': { + type: 'string', + describe: 'API key to access the API server', + require: true, + }, + 'api-url': { + type: 'string', + describe: 'URL for the API server', + require: true, + } + }); + }, + async (argv) => { + const importFilePath = (argv.IMPORT_FILE_PATH as string); + + await pushStringsFromExcelToIfrc( + importFilePath, + argv.apiUrl as string, + argv.apiKey as string, + ); + }, + ) .command( 'push-strings-dref ', 'IMPORTANT!!! Temporary command, do not use!', @@ -283,7 +315,7 @@ yargs(hideBin(process.argv)) await pushStringsDref( importFilePath, argv.apiUrl as string, - argv.authToken as string, + argv.apiKey as string, ); }, ) diff --git a/app/src/config.ts b/app/src/config.ts index ddc85d2e47..92cc521d03 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -6,6 +6,8 @@ const { APP_MAPBOX_ACCESS_TOKEN, APP_TINY_API_KEY, APP_RISK_API_ENDPOINT, + APP_TRANSLATION_API_ENDPOINT, + APP_TRANSLATION_API_KEY, APP_SDT_URL, APP_POWER_BI_REPORT_ID_1, APP_SENTRY_DSN, @@ -30,9 +32,11 @@ export const api = APP_API_ENDPOINT; export const adminUrl = APP_ADMIN_URL ?? `${api}admin/`; export const mbtoken = APP_MAPBOX_ACCESS_TOKEN; export const riskApi = APP_RISK_API_ENDPOINT; +export const translationApi = APP_TRANSLATION_API_ENDPOINT; export const sdtUrl = APP_SDT_URL; export const powerBiReportId1 = APP_POWER_BI_REPORT_ID_1; +export const translationApiKey = APP_TRANSLATION_API_KEY; export const tinyApiKey = APP_TINY_API_KEY; export const sentryAppDsn = APP_SENTRY_DSN; export const sentryTracesSampleRate = APP_SENTRY_TRACES_SAMPLE_RATE; diff --git a/app/src/utils/resolveUrl.ts b/app/src/utils/resolveUrl.ts index 8c6880a4af..5151ce9f12 100644 --- a/app/src/utils/resolveUrl.ts +++ b/app/src/utils/resolveUrl.ts @@ -1,9 +1,9 @@ // eslint-disable-next-line import/prefer-default-export -export function resolveUrl(from: string, to: string) { - const resolvedUrl = new URL(to, new URL(from, 'resolve://')); - if (resolvedUrl.protocol === 'resolve:') { - const { pathname, search, hash } = resolvedUrl; - return pathname + search + hash; - } +export function resolveUrl(base: string, endpoint: string) { + const baseSafe = base.endsWith('/') ? base : `${base}/`; + const endpointSafe = endpoint.startsWith('.') ? endpoint : `.${endpoint}`; + + const resolvedUrl = new URL(endpointSafe, baseSafe); + return resolvedUrl.toString(); } diff --git a/app/src/utils/restRequest/go.ts b/app/src/utils/restRequest/go.ts index 477822b687..d31af707fd 100644 --- a/app/src/utils/restRequest/go.ts +++ b/app/src/utils/restRequest/go.ts @@ -9,6 +9,8 @@ import { type ContextInterface } from '@togglecorp/toggle-request'; import { api, riskApi, + translationApi, + translationApiKey, } from '#config'; import { type UserAuth } from '#contexts/user'; import { @@ -39,8 +41,10 @@ export interface TransformedError { debugMessage: string; } +type ApiType = 'go' | 'risk' | 'translation'; + export interface AdditionalOptions { - apiType?: 'go' | 'risk'; + apiType?: ApiType; formData?: boolean; isCsvRequest?: boolean; enforceEnglishForQuery?: boolean; @@ -111,6 +115,18 @@ type GoContextInterface = ContextInterface< AdditionalOptions >; +function getEndPoint(apiType: ApiType | undefined) { + if (apiType === 'risk') { + return riskApi; + } + + if (apiType === 'translation') { + return translationApi; + } + + return api; +} + export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additionalOptions) => { if (isFalsyString(url)) { return ''; @@ -123,10 +139,12 @@ export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additi const { apiType } = additionalOptions; - return resolveUrl( - apiType === 'risk' ? riskApi : api, - url, + const resolvedUrl = resolveUrl( + getEndPoint(apiType), + `.${url}`, ); + + return resolvedUrl; }; type Literal = string | number | boolean | File; @@ -164,6 +182,7 @@ export const processGoOptions: GoContextInterface['transformOptions'] = ( } = requestOptions; const { + apiType, formData, isCsvRequest, isExcelRequest, @@ -176,10 +195,15 @@ export const processGoOptions: GoContextInterface['transformOptions'] = ( const user = getFromStorage(KEY_USER_STORAGE); const token = user?.token; + // FIXME: only inject on go apis const defaultHeaders: HeadersInit = { Authorization: token ? `Token ${token}` : '', }; + if (apiType === 'translation') { + defaultHeaders['x-api-key'] = translationApiKey; + } + if (method === 'GET') { // Query defaultHeaders['Accept-Language'] = enforceEnglishForQuery ? 'en' : currentLanguage; @@ -239,7 +263,17 @@ const isSuccessfulStatus = (status: number): boolean => status >= 200 && status const isContentTypeExcel = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_EXCEL; -const isContentTypeJson = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_JSON; +const isContentTypeJson = (res: Response): boolean => { + const contentTypeHeaders = res.headers.get('content-type'); + + if (isNotDefined(contentTypeHeaders)) { + return false; + } + + const mediaTypes = contentTypeHeaders.split('; '); + + return mediaTypes[0]?.toLowerCase() === CONTENT_TYPE_JSON; +}; const isLoginRedirect = (url: string): boolean => new URL(url).pathname.includes('login'); diff --git a/app/src/utils/restRequest/index.ts b/app/src/utils/restRequest/index.ts index ad31e30028..95bd556773 100644 --- a/app/src/utils/restRequest/index.ts +++ b/app/src/utils/restRequest/index.ts @@ -5,7 +5,9 @@ import { } from '@togglecorp/toggle-request'; import type { paths as riskApiPaths } from '#generated/riskTypes'; +// import type { paths as translationApiPaths } from '#generated/translationTypes'; import type { paths as goApiPaths } from '#generated/types'; +import type { paths as translationApiPaths } from '#translationTypes'; import type { ApiBody, @@ -23,19 +25,31 @@ export type GoApiUrlQuery = ApiBody export type RiskApiResponse = ApiResponse; -// type RiskApiUrlQuery< -// URL extends keyof riskApiPaths, -// METHOD extends 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET' -// > = ApiUrlQuery -// type RiskApiBody< -// URL extends keyof riskApiPaths, -// METHOD extends 'POST' | 'PUT' | 'PATCH' -// > = ApiBody export type ListResponseItem } | undefined> = NonNullable['results']>[number]; +/* +const useTranslationRequest = useRequest as < + PATH extends keyof translationApiPaths, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomRequestOptions & { + apiType: 'translation' + } +) => CustomRequestReturn; +*/ + +// FIXME: identify a way to do this without a cast +const useTranslationLazyRequest = useLazyRequest as < + PATH extends keyof translationApiPaths, + CONTEXT = unknown, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomLazyRequestOptions & { apiType: 'translation' } +) => CustomLazyRequestReturn; + // FIXME: identify a way to do this without a cast const useGoRequest = useRequest as < PATH extends keyof goApiPaths, @@ -76,4 +90,6 @@ export { useGoRequest as useRequest, useRiskLazyRequest, useRiskRequest, + useTranslationLazyRequest, + // useTranslationRequest, }; diff --git a/app/src/views/RootLayout/index.tsx b/app/src/views/RootLayout/index.tsx index 2a2ee6b52a..512d047428 100644 --- a/app/src/views/RootLayout/index.tsx +++ b/app/src/views/RootLayout/index.tsx @@ -28,10 +28,9 @@ import { _cs, isDefined, isFalsyString, - listToGroupList, + isNotDefined, listToMap, mapToList, - mapToMap, } from '@togglecorp/fujs'; import GlobalFooter from '#components/GlobalFooter'; @@ -46,8 +45,8 @@ import UserContext from '#contexts/user'; import useAuth from '#hooks/domain/useAuth'; import useDebouncedValue from '#hooks/useDebouncedValue'; import { - useLazyRequest, useRequest, + useTranslationLazyRequest, } from '#utils/restRequest'; import i18n from './i18n.json'; @@ -99,66 +98,57 @@ export function Component() { const { trigger: fetchLanguage, - } = useLazyRequest<'/api/v2/language/{id}/', { pages: Array }>({ - url: '/api/v2/language/{id}/', + } = useTranslationLazyRequest<'/api/Application/{applicationId}/Translation', { page: string }>({ + apiType: 'translation', + url: '/api/Application/{applicationId}/Translation', // FIXME: fix typing in server (medium priority) - query: ({ pages }) => ({ page_name: pages }) as never, - pathVariables: () => ({ id: currentLanguage }), - onSuccess: (response, { pages }) => { - const stringMap = mapToMap( - listToGroupList( - response.strings?.map(({ value, page_name, ...otherArgs }) => { - // NOTE: removing empty translations or translations without pages - if (isFalsyString(value) || isFalsyString(page_name)) { - return undefined; - } - return { - value, - page_name, - ...otherArgs, - }; - }).filter(isDefined), - ({ page_name }) => page_name ?? 'common', - ), - (key) => key, - (values) => ( - listToMap( - values, - ({ key }) => key, - ({ value }) => value, - ) - ), - ); - + query: ({ page }) => ({ + Offset: 0, + Limit: 1000, + KeyPage: page, + LanguageCode: currentLanguage, + }), + pathVariables: () => ({ applicationId: 18 }), + onSuccess: (response, { page }) => { setStrings( - (prevValue) => { - const namespaces = Object.keys(prevValue); - - return { - ...listToMap( - namespaces, - (namespace) => namespace, - (namespace) => ({ - ...prevValue[namespace], - ...stringMap?.[namespace], - }), - ), + (prevStrings) => { + const newStrings = { + ...prevStrings, + [page]: { + ...prevStrings[page], + ...listToMap( + (response.values ?? []).map((item) => { + const { keyName, value } = item; + + if (isNotDefined(keyName) || isNotDefined(value)) { + return undefined; + } + + return { + keyName, + value, + }; + }).filter(isDefined), + ({ keyName }) => keyName, + ({ value }) => value, + ), + }, }; + + return newStrings; }, ); + setLanguageNamespaceStatus( (prevValue) => ({ ...prevValue, - ...listToMap( - pages, - (key) => key, - () => 'fetched', - ), + [page]: 'fetched', }), ); + setLanguagePending(false); }, - onFailure: (err, { pages }) => { + onFailure: (err, { page }) => { // eslint-disable-next-line no-console console.error(err); @@ -166,62 +156,49 @@ export function Component() { setLanguageNamespaceStatus( (prevValue) => ({ ...prevValue, - ...listToMap( - pages, - (key) => key, - () => 'failed', - ), + [page]: 'failed', }), ); setLanguagePending(false); }, }); - const queuedLanguages = useMemo( - () => { - const languages = mapToList( - languageNamespaceStatus, - (item, key) => ({ key, status: item }), - ); - return languages - .filter((item) => item.status === 'queued') - .map((item) => item.key) - .sort() - .join(','); - }, + const queuedNamespaces = useMemo( + () => mapToList( + languageNamespaceStatus, + (item, key) => ({ key, status: item }), + ).filter( + (item) => item.status === 'queued', + ).map((item) => item.key).sort(), [languageNamespaceStatus], ); + const queuedNamespace = queuedNamespaces[0]; + useEffect( () => { if ( languagePending || currentLanguage === 'en' - || isFalsyString(queuedLanguages) + || isFalsyString(queuedNamespace) ) { return undefined; } languageRequestTimeoutRef.current = window.setTimeout( () => { - const keys = queuedLanguages.split(','); - unstable_batchedUpdates(() => { // FIXME: check if the component is still mounted setLanguageNamespaceStatus( (prevState) => ({ ...prevState, - ...listToMap( - keys, - (key) => key, - () => 'pending', - ), + [queuedNamespace]: 'pending', }), ); setLanguagePending(true); }); - fetchLanguage({ pages: keys }); + fetchLanguage({ page: queuedNamespace }); }, // FIXME: use constant 200, @@ -232,7 +209,7 @@ export function Component() { }; }, [ - queuedLanguages, + queuedNamespace, languagePending, currentLanguage, fetchLanguage, diff --git a/app/translationTypes.ts b/app/translationTypes.ts new file mode 100644 index 0000000000..52d328f9da --- /dev/null +++ b/app/translationTypes.ts @@ -0,0 +1,586 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-nocheck +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + + +export interface paths { + "/api/Application": { + get: operations["Application_List"]; + post: operations["Application_Register"]; + }; + "/api/Application/{applicationId}": { + get: operations["Application_Get"]; + put: operations["Application_Update"]; + }; + "/api/Application/{applicationId}/Key": { + get: operations["Key_List"]; + }; + "/api/Application/{applicationId}/Page": { + get: operations["Page_List"]; + }; + "/api/Application/{applicationId}/Translation": { + get: operations["Translation_ListTranslations"]; + post: operations["Translation_CreateTranslation"]; + }; + "/api/Application/{applicationId}/Translation/{translationId}": { + get: operations["Translation_GetTranslation"]; + put: operations["Translation_UpdateTranslation"]; + delete: operations["Translation_DeleteTranslation"]; + }; + "/api/Application/{applicationId}/Translation/checkimport": { + post: operations["Translation_CheckImport"]; + }; + "/api/Application/{applicationId}/Translation/export": { + get: operations["Translation_ExportTranslations"]; + }; + "/api/Application/{applicationId}/Translation/fullappimport": { + post: operations["Translation_FullAppImport"]; + }; + "/api/Application/{applicationId}/Translation/import": { + post: operations["Translation_ImportTranslations"]; + }; + "/api/Application/{applicationId}/TranslationKey": { + get: operations["TranslationKey_List"]; + }; + "/api/Application/{applicationId}/TranslationKey/{translationKeyId}": { + get: operations["TranslationKey_Get"]; + }; + "/api/AutomaticTranslation": { + post: operations["AutomaticTranslation_StartAutomaticTranslation"]; + }; + "/api/Detect": { + post: operations["Home_Detect"]; + }; + "/api/Language": { + get: operations["Language_List"]; + }; + "/api/Translate": { + post: operations["Home_Translate"]; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: { + ApplicationRegistrationViewModel: { + /** Format: int32 */ + applicationId: number; + automaticTranslationEnabled?: boolean; + languageCodes?: string[]; + name: string; + }; + ApplicationUpdateViewModel: { + automaticTranslationEnabled?: boolean; + languageCodes?: string[]; + name: string; + }; + ApplicationViewModel: { + automaticTranslationEnabled?: boolean; + /** Format: int32 */ + id?: number; + languages: components["schemas"]["LanguageViewModel"][]; + name: string; + }; + DetectionRequestModel: { + text?: string; + }; + DetectionResult: components["schemas"]["DetectionResultBase"] & { + alternatives?: components["schemas"]["DetectionResultBase"][]; + }; + DetectionResultBase: { + isTranslationSupported?: boolean; + isTransliterationSupported?: boolean; + language?: string; + /** Format: float */ + score?: number; + }; + LanguageViewModel: { + code?: string; + isActive?: boolean; + localeName?: string | null; + name?: string; + }; + ListResultViewModelOfTranslationKeyViewModel: { + /** Format: int64 */ + count?: number; + page?: components["schemas"]["PagingViewModel"]; + values?: components["schemas"]["TranslationKeyViewModel"][]; + }; + ListResultViewModelOfTranslationViewModel: { + /** Format: int64 */ + count?: number; + page?: components["schemas"]["PagingViewModel"]; + values?: components["schemas"]["TranslationViewModel"][]; + }; + PagingViewModel: { + /** Format: int32 */ + limit?: number; + /** Format: int32 */ + offset?: number; + }; + ProblemDetails: { + detail?: string | null; + instance?: string | null; + /** Format: int32 */ + status?: number | null; + title?: string | null; + type?: string | null; + [key: string]: (Record | null) | undefined; + }; + TranslationConfig: { + allowedAddresses?: string | null; + apikey?: string; + disabled?: boolean; + endPoint?: string; + /** Format: int32 */ + id?: number; + key1?: string; + key2?: string; + region?: string; + }; + TranslationCreateViewModel: { + keyName: string; + languageCode: string; + page: string; + value: string; + }; + TranslationImportAction: { + isSuccess?: boolean; + key?: string; + languageCode?: string | null; + message?: string; + newValue?: string | null; + originalValue?: string | null; + page?: string; + }; + TranslationImportResult: { + isSuccess?: boolean; + message?: string; + translationActions?: components["schemas"]["TranslationImportAction"][]; + }; + TranslationKeyDetailsTranslationViewModel: { + /** Format: int32 */ + id?: number; + isAutomaticTranslation?: boolean; + languageCode?: string; + value?: string; + }; + TranslationKeyDetailsViewModel: { + /** Format: int32 */ + applicationId?: number; + applicationName?: string; + /** Format: int32 */ + id?: number; + name?: string; + page?: string; + values?: components["schemas"]["TranslationKeyDetailsTranslationViewModel"][]; + }; + TranslationKeyViewModel: { + /** Format: int32 */ + applicationId?: number; + /** Format: int32 */ + id?: number; + name?: string; + page?: string; + translatedTo?: string[]; + }; + TranslationRequestModel: { + from?: string | null; + text?: string; + textType?: string | null; + to?: string; + }; + TranslationUpdateViewModel: { + value: string; + }; + TranslationViewModel: { + /** Format: int32 */ + applicationId?: number; + applicationName?: string; + /** Format: int32 */ + id?: number; + isAutomaticTranslation?: boolean; + /** Format: int32 */ + keyId?: number; + keyName?: string; + keyPage?: string; + languageCode?: string; + languageName?: string; + value?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} + +export type $defs = Record; + +export type external = Record; + +export interface operations { + + Application_List: { + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ApplicationViewModel"][]; + }; + }; + }; + }; + Application_Register: { + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["ApplicationRegistrationViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/json": number; + }; + }; + }; + }; + Application_Get: { + parameters: { + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ApplicationViewModel"]; + }; + }; + }; + }; + Application_Update: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["ApplicationUpdateViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Key_List: { + parameters: { + query?: { + page?: string | null; + }; + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": string[]; + }; + }; + }; + }; + Page_List: { + parameters: { + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": string[]; + }; + }; + }; + }; + Translation_ListTranslations: { + parameters: { + query?: { + KeyName?: string | null; + KeyPage?: string | null; + LanguageCode?: string | null; + TranslationId?: number | null; + Offset?: number; + Limit?: number | null; + Value?: string | null; + }; + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ListResultViewModelOfTranslationViewModel"]; + }; + }; + }; + }; + Translation_CreateTranslation: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["TranslationCreateViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_GetTranslation: { + parameters: { + path: { + applicationId: number; + translationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationViewModel"]; + }; + }; + 404: { + content: { + "application/json": components<'read'>["schemas"]["ProblemDetails"]; + }; + }; + }; + }; + Translation_UpdateTranslation: { + parameters: { + path: { + applicationId: number; + translationId: number; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["TranslationUpdateViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_DeleteTranslation: { + parameters: { + path: { + applicationId: number; + translationId: number; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_CheckImport: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody?: { + content: { + "multipart/form-data": { + files?: string[] | null; + }; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationImportResult"]; + }; + }; + }; + }; + Translation_ExportTranslations: { + parameters: { + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_FullAppImport: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody?: { + content: { + "multipart/form-data": { + files?: string[] | null; + }; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationImportResult"]; + }; + }; + }; + }; + Translation_ImportTranslations: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody?: { + content: { + "multipart/form-data": { + files?: string[] | null; + }; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationImportResult"]; + }; + }; + }; + }; + TranslationKey_List: { + parameters: { + query?: { + Name?: string | null; + Page?: string | null; + Limit?: number | null; + Offset?: number; + }; + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ListResultViewModelOfTranslationKeyViewModel"]; + }; + }; + }; + }; + TranslationKey_Get: { + parameters: { + path: { + applicationId: number; + translationKeyId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationKeyDetailsViewModel"]; + }; + }; + }; + }; + AutomaticTranslation_StartAutomaticTranslation: { + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Home_Detect: { + parameters: { + query?: { + apiKey?: string; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["DetectionRequestModel"]; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["DetectionResult"]; + }; + }; + }; + }; + Language_List: { + parameters: { + query?: { + IsActive?: boolean | null; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["LanguageViewModel"][]; + }; + }; + }; + }; + Home_Translate: { + parameters: { + header?: { + "x-api-key"?: string; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["TranslationRequestModel"]; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationConfig"]; + }; + }; + }; + }; +} diff --git a/app/tsconfig.json b/app/tsconfig.json index 752b896862..b76e64fe2b 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -15,7 +15,8 @@ "#strings/*": ["./src/strings/*"], "#utils/*": ["./src/utils/*"], "#views/*": ["./src/views/*"], - "#routes": ["./src/App/routes"] + "#routes": ["./src/App/routes"], + "#translationTypes": ["./translationTypes.ts"] }, "target": "ESNext", diff --git a/nginx-serve/Dockerfile b/nginx-serve/Dockerfile index 8c095eaaf7..5d924cae04 100644 --- a/nginx-serve/Dockerfile +++ b/nginx-serve/Dockerfile @@ -27,6 +27,8 @@ ENV APP_ENVIRONMENT=APP_ENVIRONMENT_PLACEHOLDER ENV APP_MAPBOX_ACCESS_TOKEN=APP_MAPBOX_ACCESS_TOKEN_PLACEHOLDER ENV APP_TINY_API_KEY=APP_TINY_API_KEY_PLACEHOLDER ENV APP_API_ENDPOINT=https://APP-API-ENDPOINT-PLACEHOLDER.COM/ +ENV APP_TRANSLATION_API_ENDPOINT=https://APP-TRANSLATION-API-ENDPOINT-PLACEHOLDER.COM/ +ENV APP_TRANSLATION_API_KEY=APP_TRANSLATION_API_KEY_PLACEHOLDER ENV APP_RISK_API_ENDPOINT=https://APP-RISK-API-ENDPOINT-PLACEHOLDER.COM/ ENV APP_SDT_URL=https://APP-SDT-URL-PLACEHOLDER.COM/ ENV APP_SENTRY_DSN=https://APP-SENTRY-DSN-PLACEHOLDER.COM/ diff --git a/nginx-serve/apply-config.sh b/nginx-serve/apply-config.sh index 1361d3390e..b6c010f3ef 100755 --- a/nginx-serve/apply-config.sh +++ b/nginx-serve/apply-config.sh @@ -28,8 +28,10 @@ find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\| find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_ENVIRONMENT|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_MAPBOX_ACCESS_TOKEN|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_TINY_API_KEY|g" {} + +find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_TRANSLATION_API_KEY|g" {} + # NOTE: We don't need a word boundary at end as we already have a trailing slash find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\