diff --git a/cloudformation/ecs-service.yaml b/cloudformation/ecs-service.yaml index 5757e8c..7523e6f 100644 --- a/cloudformation/ecs-service.yaml +++ b/cloudformation/ecs-service.yaml @@ -185,6 +185,11 @@ Resources: Action: - s3:* Resource: !Sub arn:aws:s3:::${UploadBucket} + - Effect: Allow + Action: + - 'comprehend:DetectDominantLanguage' + - 'translate:TranslateText' + Resource: '*' # A role for the containers TaskRole: diff --git a/src/bernard/client.ts b/src/bernard/client.ts index 37fac67..5f264c4 100644 --- a/src/bernard/client.ts +++ b/src/bernard/client.ts @@ -25,7 +25,6 @@ export async function sendMessage( body: string, queueUrl: string, ): Promise { - console.log('Senfing', body); const client = new SQSClient({ region: 'eu-west-1' }); const params: SendMessageCommandInput = { diff --git a/src/datasources/product-api.ts b/src/datasources/product-api.ts index 5d4dafc..e72b33a 100644 --- a/src/datasources/product-api.ts +++ b/src/datasources/product-api.ts @@ -28,12 +28,15 @@ import { } from './utils'; import { UserInputError } from 'apollo-server'; import { sendPathChangedCmd } from '../bernard/client'; +import { TextsAPI } from './texts-api'; const MINUTE = 60; export class ProductAPI extends BaseSQLDataSource { - constructor(config) { + textsApi: TextsAPI; + constructor(config, textsApi) { super(config); + this.textsApi = textsApi; } // async getMaterial(printId: number): Promise { @@ -384,6 +387,14 @@ export class ProductAPI extends BaseSQLDataSource { sendPathChangedCmd(oldProd.path, uniquePath); } + if (oldProd.fields.name !== info.name) { + await this.textsApi.translateProductName( + info.name, + oldProd.path, + uniquePath, + ); + } + const promises = []; promises.push( this.knex diff --git a/src/datasources/texts-api.ts b/src/datasources/texts-api.ts new file mode 100644 index 0000000..29a653f --- /dev/null +++ b/src/datasources/texts-api.ts @@ -0,0 +1,106 @@ +import { SQLDataSource } from 'datasource-sql'; +import { TranslateInput, translateText } from '../aws/translate'; +import { Locale } from '../types/market-locale-types'; +import { MarketLocaleAPI } from './market-locale-api'; + +const MINUTE = 60; + +// TODO: perhaps rename to MarketLocale API and add market_locales and locales here. +export class TextsAPI extends SQLDataSource { + marketLocaleApi: MarketLocaleAPI; + + constructor(config, marketLocaleApi: MarketLocaleAPI) { + super(config); + this.marketLocaleApi = marketLocaleApi; + } + + async translateProductName(name: string, oldPath, newPath): Promise { + // Check if we have an entry already + const textKey = await this.upsertTextId(newPath, oldPath); + + const locales = await this.marketLocaleApi.getLocales(); + for (let i = 0; i < locales.length; i++) { + let langCode = this.getLanguageCode(locales[i]); + // If we have a fallback locale we skip translating + if (locales[i].fallbackId) { + continue; + } + const input: TranslateInput = { + text: name, + targetLanguageCode: langCode, + }; + const translated = await translateText(input); + + await this.upsertTextDataEntry(textKey.textid, translated, locales[i].id); + } + } + + /** + * Upsert translated entry to the i18n-texts_data table + * @param text The translated text to insert + * @param localeId The id of the locale + */ + private async upsertTextDataEntry( + textId: number, + text: string, + localeId: number, + ) { + // Check if we have an entry already + const textDataEntry = await this.knex + .select('*') + .from('i18n-texts_data') + .where({ textid: textId, locale_id: localeId }) + .first(); + + if (textDataEntry) { + // Update existing entry + await this.knex('i18n-texts_data') + .where({ textid: textId, locale_id: localeId }) + .update({ text: text }); + } else { + //Insert new entry + await this.knex('i18n-texts_data').insert({ + text: text, + textid: textId, + locale_id: localeId, + }); + } + } + + private getLanguageCode(locale: Locale) { + let langCode = locale.value.substr(0, 2); + return langCode == 'nn' ? 'no' : langCode; + } + + /** + * Upsert translated entry to the i18n-texts table + * @param newKey the new name of the text key + * @param localeId the old name of the text key + */ + private async upsertTextId(newKey: string, oldKey: string) { + let textEntry = await this.knex + .select('*') + .from('i18n-texts') + .where({ name: oldKey }) + .first(); + + let textId = textEntry?.textid; + // No entry, create one + if (!textId) { + const res = await this.knex('i18n-texts') + .insert({ name: newKey, category: 'productTitles' }) + .returning('*'); + + textEntry = res[0]; + } + + // Update table if different + if (oldKey !== newKey) { + await this.knex + .table('i18n-texts') + .update({ name: newKey }) + .where({ name: oldKey }); + } + return textEntry; + } +} diff --git a/src/index.ts b/src/index.ts index 1e2a4a0..6ee1016 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { InteriorAPI } from './datasources/interior-api'; import { ClaimVerifyResult, verifyToken } from './cognito/cognito-client'; import { readFileSync } from 'fs'; import { InteriorsLambdaAPI } from './datasources/interiors-lambda-api'; +import { TextsAPI } from './datasources/texts-api'; const path = require('path'); const typeDefs = readFileSync( path.join(__dirname, './schema.graphql'), @@ -27,15 +28,16 @@ const typeDefs = readFileSync( const knexConfig = knexStringcase(dbConfig); // set up any dataSources our resolvers need +const marketLocaleApi = new MarketLocaleAPI(knexConfig); +const textsApi = new TextsAPI(knexConfig, marketLocaleApi); const categoryApi = new CategoryAPI(knexConfig); const designerApi = new DesignerAPI(knexConfig); const imageServerApi = new ImageServerApi(); const interiorsLambdaApi = new InteriorsLambdaAPI(); const interiorApi = new InteriorAPI(knexConfig, imageServerApi); const keywordApi = new KeywordAPI(knexConfig); -const marketLocaleApi = new MarketLocaleAPI(knexConfig); const orderApi = new OrderAPI(knexConfig); -const productApi = new ProductAPI(knexConfig); +const productApi = new ProductAPI(knexConfig, textsApi); const dataSources = () => ({ categoryApi,